From 9c21a8a7da923f0bfd4a9fe117b1dcbcd1e31329 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 13 Feb 2025 17:39:13 -0500 Subject: [PATCH] Massive refactoring of Quartz to: - Define each tag in their own class. - Allow extension functions to additional responsibilities to other classes - Migrate from hardcoded tag filters in events to the Tag's parser and assemble functions. - Migrate hardcoded event.create to builders that use extension functions - Restructures threading infrastructure for NIP-10 - Decouple the event signing from the Event building functions via event templates - Create classes to represent Tags and TagArrays and use extension functions to add domain-related methods to the tag array of each nip. - Uses external functions on event template builders to better point to which functions and which tags can be used in which event kinds. - Separates Event kinds in packages inside each nip. - Improves support for NIP-89 - Correctly establishes which imeta params can be used in each nip (video, picture, files) - Decouples the iMeta builder from any nip. - Fixes mute list word and user removal when inserted from a different client. - Migrates the Account class to avoiding having to build each Event inside of it --- .../amethyst/ImageUploadTesting.kt | 2 +- .../vitorpamplona/amethyst/OkHttpOtsTest.kt | 15 +- .../amethyst/ThreadAssemblerTest.kt | 6 +- .../amethyst/UrlUserTagTransformationTest.kt | 2 +- .../notifications/PushMessageReceiver.kt | 2 +- .../amethyst/LocalPreferences.kt | 6 +- .../vitorpamplona/amethyst/ServiceManager.kt | 6 +- .../vitorpamplona/amethyst/model/Account.kt | 1521 +++++------------ .../amethyst/model/AccountSettings.kt | 6 +- .../vitorpamplona/amethyst/model/Channel.kt | 59 +- .../amethyst/model/LocalCache.kt | 146 +- .../com/vitorpamplona/amethyst/model/Note.kt | 94 +- .../amethyst/model/ThreadAssembler.kt | 4 +- .../com/vitorpamplona/amethyst/model/User.kt | 22 +- .../service/AmethystNostrDataSource.kt | 2 +- .../service/NostrAccountDataSource.kt | 20 +- .../service/NostrChannelDataSource.kt | 4 +- .../service/NostrChatroomDataSource.kt | 2 +- .../service/NostrChatroomListDataSource.kt | 6 +- .../service/NostrCommunityDataSource.kt | 6 +- .../service/NostrDiscoveryDataSource.kt | 16 +- .../service/NostrGeohashDataSource.kt | 6 +- .../service/NostrHashtagDataSource.kt | 8 +- .../amethyst/service/NostrHomeDataSource.kt | 12 +- .../NostrSearchEventOrUserDataSource.kt | 19 +- .../service/NostrSingleChannelDataSource.kt | 4 +- .../service/NostrSingleEventDataSource.kt | 6 +- .../service/NostrSingleUserDataSource.kt | 4 +- .../service/NostrUserProfileDataSource.kt | 8 +- .../amethyst/service/NostrVideoDataSource.kt | 2 +- .../amethyst/service/ZapPaymentHandler.kt | 6 +- .../EventNotificationConsumer.kt | 12 +- .../okhttp/EncryptedBlobInterceptor.kt | 4 +- .../service/okhttp/EncryptionKeyCache.kt | 2 +- .../service/okhttp/HttpClientManager.kt | 2 +- .../service/okhttp/OkHttpWebSocket.kt | 8 +- .../amethyst/service/uploads/EncryptFiles.kt | 2 +- .../amethyst/service/uploads/FileHeader.kt | 14 +- .../service/uploads/MediaUploadResult.kt | 4 +- .../service/uploads/MultiOrchestrator.kt | 12 +- .../service/uploads/UploadOrchestrator.kt | 22 +- .../service/uploads/nip96/Nip96Uploader.kt | 31 +- .../uploads/nip96/ServerInfoRetriever.kt | 4 +- .../vitorpamplona/amethyst/ui/MainActivity.kt | 10 +- .../amethyst/ui/actions/EditPostViewModel.kt | 22 +- .../ui/actions/NewChannelViewModel.kt | 53 +- .../amethyst/ui/actions/NewMediaModel.kt | 8 +- .../amethyst/ui/actions/NewMessageTagger.kt | 2 +- .../amethyst/ui/actions/NewPollClosing.kt | 2 +- .../amethyst/ui/actions/NewPostViewModel.kt | 829 +++++---- .../ui/actions/relays/AddDMRelayListDialog.kt | 2 +- .../relays/AddSearchRelayListDialog.kt | 2 +- .../ui/actions/relays/AllRelayListView.kt | 2 +- .../ui/actions/relays/BasicRelaySetupInfo.kt | 2 +- .../ui/actions/relays/Kind3RelayListView.kt | 2 +- .../relays/Kind3RelaySetupInfoProposalRow.kt | 2 +- .../ui/actions/relays/RelayUrlEditField.kt | 2 +- .../amethyst/ui/components/ClickableRoute.kt | 2 +- .../amethyst/ui/components/VideoView.kt | 22 +- .../ui/components/ZoomableContentView.kt | 6 +- .../markdown/MarkdownMediaRenderer.kt | 9 +- .../markdown/RenderContentAsMarkdown.kt | 5 +- .../amethyst/ui/dal/ChatroomFeedFilter.kt | 2 +- .../ui/dal/ChatroomListKnownFeedFilter.kt | 6 +- .../ui/dal/ChatroomListNewFeedFilter.kt | 4 +- .../amethyst/ui/dal/CommunityFeedFilter.kt | 2 +- .../amethyst/ui/dal/DiscoverChatFeedFilter.kt | 6 +- .../ui/dal/DiscoverCommunityFeedFilter.kt | 4 +- .../amethyst/ui/dal/DiscoverLiveFeedFilter.kt | 12 +- .../ui/dal/DiscoverNIP89FeedFilter.kt | 4 +- .../amethyst/ui/dal/FilterByListParams.kt | 2 +- .../amethyst/ui/dal/GeoHashFeedFilter.kt | 4 +- .../amethyst/ui/dal/HashtagFeedFilter.kt | 4 +- .../ui/dal/HomeConversationsFeedFilter.kt | 4 +- .../ui/dal/HomeNewThreadFeedFilter.kt | 4 +- .../amethyst/ui/dal/NotificationFeedFilter.kt | 18 +- ...UserProfileAppRecommendationsFeedFilter.kt | 5 +- .../dal/UserProfileConversationsFeedFilter.kt | 4 +- .../ui/dal/UserProfileNewThreadFeedFilter.kt | 4 +- .../amethyst/ui/dal/VideoFeedFilter.kt | 2 +- .../amethyst/ui/navigation/RouteMaker.kt | 30 +- .../amethyst/ui/note/ChannelCardCompose.kt | 51 +- .../amethyst/ui/note/MessageSetCompose.kt | 20 +- .../ui/note/NIP05VerificationDisplay.kt | 2 +- .../amethyst/ui/note/NoteCompose.kt | 46 +- .../amethyst/ui/note/NoteQuickActionMenu.kt | 6 +- .../amethyst/ui/note/PollNote.kt | 6 +- .../amethyst/ui/note/PollNoteViewModel.kt | 17 +- .../amethyst/ui/note/ReactionsRow.kt | 4 +- .../ui/note/ShowEmojiSuggestionList.kt | 2 +- .../ui/note/UpdateReactionTypeDialog.kt | 12 +- .../amethyst/ui/note/UserProfilePicture.kt | 2 +- .../note/elements/AddInboxRelayForDMCard.kt | 2 +- .../ui/note/elements/DisplayCommunity.kt | 2 +- .../ui/note/elements/DisplayReward.kt | 25 +- .../amethyst/ui/note/elements/ForkInfo.kt | 8 +- .../amethyst/ui/note/types/AppDefinition.kt | 4 +- .../amethyst/ui/note/types/AudioTrack.kt | 21 +- .../amethyst/ui/note/types/Badge.kt | 2 +- .../amethyst/ui/note/types/ChannelMessage.kt | 4 +- .../amethyst/ui/note/types/ChatMessage.kt | 2 +- .../ui/note/types/ChatMessageEncryptedFile.kt | 7 +- .../amethyst/ui/note/types/CommunityHeader.kt | 8 +- .../amethyst/ui/note/types/Emoji.kt | 10 +- .../amethyst/ui/note/types/FileHeader.kt | 1 + .../amethyst/ui/note/types/FileStorage.kt | 3 +- .../amethyst/ui/note/types/Git.kt | 6 +- .../amethyst/ui/note/types/Highlight.kt | 6 +- .../ui/note/types/InteractiveStory.kt | 2 +- .../amethyst/ui/note/types/LiveActivity.kt | 15 +- .../ui/note/types/LiveActivityChatMessage.kt | 2 +- .../amethyst/ui/note/types/PeopleList.kt | 2 +- .../amethyst/ui/note/types/Poll.kt | 2 +- .../amethyst/ui/note/types/PrivateMessage.kt | 2 +- .../amethyst/ui/note/types/RelayList.kt | 2 +- .../ui/note/types/RenderPostApproval.kt | 2 +- .../amethyst/ui/note/types/Text.kt | 8 +- .../ui/note/types/TextModification.kt | 2 +- .../amethyst/ui/note/types/Torrent.kt | 4 +- .../amethyst/ui/note/types/TorrentComment.kt | 2 +- .../amethyst/ui/note/types/Video.kt | 1 + .../amethyst/ui/note/types/VideoDisplay.kt | 1 + .../ui/screen/AccountStateViewModel.kt | 13 +- .../amethyst/ui/screen/FeedViewModel.kt | 2 +- .../amethyst/ui/screen/FollowListState.kt | 10 +- .../ui/screen/loggedIn/AccountViewModel.kt | 42 +- .../ui/screen/loggedIn/LoadRedirectScreen.kt | 22 +- .../ui/screen/loggedIn/NewPostScreen.kt | 12 +- .../loggedIn/chatrooms/ChannelScreen.kt | 136 +- .../loggedIn/chatrooms/ChatFileUploadModel.kt | 52 +- .../chatrooms/ChatroomHeaderCompose.kt | 8 +- .../chatrooms/ChatroomMessageCompose.kt | 10 +- .../loggedIn/chatrooms/ChatroomScreen.kt | 70 +- .../loggedIn/discover/DiscoverScreen.kt | 8 +- .../dvms/DvmContentDiscoveryScreen.kt | 4 +- .../notifications/CardFeedContentState.kt | 6 +- .../notifications/NotificationSummaryState.kt | 6 +- .../screen/loggedIn/profile/ProfileScreen.kt | 2 +- .../ui/screen/loggedIn/qrcode/ShowQRDialog.kt | 2 +- .../loggedIn/threadview/ThreadFeedView.kt | 36 +- .../ui/screen/loggedIn/video/VideoScreen.kt | 2 +- .../src/main/res/values-cy-rGB/strings.xml | 2 +- .../src/main/res/values-da-rDK/strings.xml | 2 +- .../src/main/res/values-en-rGB/strings.xml | 2 +- .../src/main/res/values-gu-rIN/strings.xml | 2 +- .../src/main/res/values-hr-rHR/strings.xml | 2 +- .../src/main/res/values-iw-rIL/strings.xml | 2 +- .../src/main/res/values-kk-rKZ/strings.xml | 2 +- .../src/main/res/values-ko-rKR/strings.xml | 2 +- .../src/main/res/values-ks-rIN/strings.xml | 2 +- .../src/main/res/values-lt-rLT/strings.xml | 2 +- .../src/main/res/values-ne-rNP/strings.xml | 2 +- .../src/main/res/values-pcm-rNG/strings.xml | 2 +- .../src/main/res/values-pt-rPT/strings.xml | 2 +- .../src/main/res/values-ru-rUA/strings.xml | 2 +- .../src/main/res/values-sa-rIN/strings.xml | 2 +- .../src/main/res/values-sr-rSP/strings.xml | 2 +- .../src/main/res/values-sw-rTZ/strings.xml | 2 +- amethyst/src/main/res/values-tr/strings.xml | 2 +- .../src/main/res/values-uz-rUZ/strings.xml | 2 +- .../src/main/res/values-vi-rVN/strings.xml | 2 +- .../src/main/res/values-zh-rSG/strings.xml | 2 +- .../PushNotificationReceiverService.kt | 2 +- .../ui/actions/NewPostViewModelTest.kt | 5 +- .../ammolite/relays/NostrClient.kt | 4 +- .../ammolite/relays/NostrDataSource.kt | 2 +- .../vitorpamplona/ammolite/relays/Relay.kt | 14 +- .../ammolite/relays/RelayPool.kt | 2 +- .../ammolite/relays/RelayStats.kt | 2 +- .../relays/filters/IPerRelayFilter.kt | 2 +- .../filters/SinceAuthorPerRelayFilter.kt | 6 +- .../relays/filters/SincePerRelayFilter.kt | 6 +- .../benchmark/RichTextParserBenchmark.kt | 2 +- .../quartz/benchmark/CryptoBenchmark.kt | 2 +- .../quartz/benchmark/EventBenchmark.kt | 4 +- .../benchmark/EventCmdHasherBenchmark.kt | 6 +- .../quartz/benchmark/GiftWrapBenchmark.kt | 24 +- .../benchmark/GiftWrapReceivingBenchmark.kt | 70 +- .../benchmark/GiftWrapSigningBenchmark.kt | 108 +- .../commons/richtext/MediaContentModels.kt | 20 +- .../commons/richtext/RichTextParser.kt | 44 +- .../vitorpamplona/quartz/CryptoUtilsTest.kt | 2 +- .../quartz/LargeDBSignatureCheck.kt | 6 +- .../quartz/nip01Core/Nip01Test.kt | 1 + .../quartz/nip03Timestamp/ots/OtsTest.kt | 27 +- .../{Nip04Test.kt => Nip04EncryptionTest.kt} | 14 +- .../quartz/nip10Notes/CitationTests.kt | 172 +- .../quartz/nip10Notes/ThreadingTests.kt | 340 ++++ .../quartz/nip17Dm/AESGCMTest.kt | 1 + .../quartz/nip17Dm/ChatroomKeyTest.kt | 1 + .../quartz/nip19Bech32/NIP19EmbedTests.kt | 22 +- .../quartz/nip44Encryption/NIP44v2Test.kt | 4 +- .../quartz/nip46RemoteSigner/Nip46Test.kt | 6 +- .../quartz/nip57Zaps/PrivateZapTests.kt | 2 +- .../nip59GiftWraps/GiftWrapEventTest.kt | 45 +- .../com/vitorpamplona/quartz/CryptoUtils.kt | 13 +- .../com/vitorpamplona/quartz/EventFactory.kt | 58 +- .../quartz/blossom/BlossomServersEvent.kt | 4 +- .../experimental/audio/AudioHeaderEvent.kt | 88 - .../audio/header/AudioHeaderEvent.kt | 68 + .../audio/header/TagArrayBuilderExt.kt | 32 + .../audio/header/tags/DownloadUrlTag.kt | 37 + .../audio/header/tags/StreamUrlTag.kt | 37 + .../audio/header/tags/WaveformTag.kt | 47 + .../audio/{ => track}/AudioTrackEvent.kt | 65 +- .../audio/track/TagArrayBuilderExt.kt | 38 + .../experimental/audio/track/tags/CoverTag.kt | 37 + .../experimental/audio/track/tags/MediaTag.kt | 37 + .../audio/track/tags/ParticipantTag.kt | 61 + .../experimental/audio/track/tags/PriceTag.kt | 37 + .../experimental/audio/track/tags/TypeTag.kt | 37 + .../bounties/BountyAddValueEvent.kt | 60 + .../quartz/experimental/bounties/EventExt.kt | 12 +- .../quartz/experimental/bounties/RewardTag.kt | 40 + .../experimental/bounties/TagArrayExt.kt | 29 + .../edits/PrivateOutboxRelayListEvent.kt | 4 +- .../edits/TextNoteModificationEvent.kt | 4 +- .../forks/BaseThreadedEventExt.kt | 40 + .../experimental/forks/MarkedETagExt.kt | 38 + .../inlineMetadata/Nip54InlineMetadata.kt | 14 +- .../InteractiveStoryBaseEvent.kt | 91 +- .../InteractiveStoryPrologueEvent.kt | 41 +- .../InteractiveStoryReadingStateEvent.kt | 102 +- .../InteractiveStorySceneEvent.kt | 35 +- .../interactiveStories/TagArrayBuilderExt.kt | 58 + .../interactiveStories/tags/ReadStatusTag.kt | 48 + .../interactiveStories/tags/RootSceneTag.kt | 94 + .../interactiveStories/tags/StoryOptionTag.kt | 49 + .../experimental/limits/LimitProcessor.kt | 2 +- .../experimental/medical/FhirResourceEvent.kt | 18 +- .../nip95/FileStorageHeaderEvent.kt | 115 -- .../nip95/{ => data}/FileStorageEvent.kt | 45 +- .../nip95/data/TagArrayBuilderExt.kt | 26 + .../nip95/header/FileStorageHeaderEvent.kt | 104 ++ .../nip95/header/TagArrayBuilderExt.kt | 60 + .../quartz/experimental/nns/NNSEvent.kt | 35 +- .../experimental/nns/TagArrayBuilderExt.kt | 32 + .../quartz/experimental/nns/tags/IPv4Tag.kt | 37 + .../quartz/experimental/nns/tags/IPv6Tag.kt | 37 + .../experimental/nns/tags/VersionTag.kt | 37 + .../profileGallery/GalleryListEvent.kt | 4 +- .../ProfileGalleryEntryEvent.kt | 121 +- .../profileGallery/TagArrayBuilderExt.kt | 72 + .../RelationshipStatusEvent.kt | 59 +- .../relationshipStatus/TagArrayBuilderExt.kt | 32 + .../relationshipStatus/tags/PetnameTag.kt | 37 + .../relationshipStatus/tags/RankTag.kt | 37 + .../relationshipStatus/tags/SummaryTag.kt | 37 + .../experimental/zapPolls/PollNoteEvent.kt | 134 +- .../zapPolls/TagArrayBuilderExt.kt | 45 + .../experimental/zapPolls/tags/ClosedAtTag.kt | 37 + .../zapPolls/tags/ConsensusThresholdTag.kt | 39 + .../experimental/zapPolls/tags/MaximumTag.kt | 37 + .../experimental/zapPolls/tags/MinimumTag.kt | 37 + .../zapPolls/tags/PollOptionTag.kt | 48 + .../quartz/nip01Core/EventExt.kt | 12 +- .../quartz/nip01Core/EventHintBundle.kt | 21 +- .../quartz/nip01Core/core/AddressableEvent.kt | 7 +- .../nip01Core/core/BaseAddressableEvent.kt | 5 +- .../nip01Core/core/BaseReplaceableEvent.kt | 5 +- .../quartz/nip01Core/core/Event.kt | 2 +- .../Price.kt => nip01Core/core/IEvent.kt} | 8 +- .../quartz/nip01Core/core/Tag.kt | 80 + .../quartz/nip01Core/core/TagArray.kt | 64 +- .../quartz/nip01Core/core/TagArrayBuilder.kt | 70 +- .../quartz/nip01Core/{ => crypto}/KeyPair.kt | 3 +- .../quartz/nip01Core/{ => crypto}/Nip01.kt | 2 +- .../quartz/nip01Core/jackson/EventMapper.kt | 6 +- .../nip01Core/{ => metadata}/MetadataEvent.kt | 9 +- .../nip01Core/{ => metadata}/UserMetadata.kt | 2 +- .../nip01Core/{relays => relay}/RelayStat.kt | 2 +- .../nip01Core/{relays => relay}/RelayState.kt | 2 +- .../{relays => relay}/SimpleClientRelay.kt | 36 +- .../{relays => relay}/Subscription.kt | 4 +- .../SubscriptionCollection.kt | 4 +- .../commands/toClient/AuthMessage.kt | 2 +- .../commands/toClient/ClosedMessage.kt | 2 +- .../commands/toClient/EoseMessage.kt | 2 +- .../commands/toClient/EventMessage.kt | 2 +- .../commands/toClient/Message.kt | 2 +- .../commands/toClient/NoticeMessage.kt | 2 +- .../commands/toClient/NotifyMessage.kt | 2 +- .../commands/toClient/OkMessage.kt | 2 +- .../commands/toClient/ToClientParser.kt | 2 +- .../commands/toRelay/AuthCmd.kt | 2 +- .../commands/toRelay/CloseCmd.kt | 2 +- .../commands/toRelay/Command.kt | 2 +- .../commands/toRelay/CountCmd.kt | 6 +- .../commands/toRelay/EventCmd.kt | 2 +- .../commands/toRelay/ReqCmd.kt | 12 +- .../commands/toRelay/ToRelayParser.kt | 2 +- .../{relays => relay}/filters/Filter.kt | 2 +- .../filters/FilterDeserializer.kt | 2 +- .../filters/FilterMatcher.kt | 2 +- .../filters/FilterSerializer.kt | 2 +- .../{relays => relay}/sockets/WebSocket.kt | 2 +- .../sockets/WebSocketListener.kt | 2 +- .../sockets/WebsocketBuilder.kt | 2 +- .../sockets/WebsocketBuilderFactory.kt | 2 +- .../quartz/nip01Core/signers/EventTemplate.kt | 48 + .../quartz/nip01Core/signers/NostrSigner.kt | 14 +- .../nip01Core/signers/NostrSignerInternal.kt | 2 +- .../nip01Core/signers/NostrSignerSync.kt | 2 +- .../nip01Core/tags/addressables/ATag.kt | 88 +- .../nip01Core/tags/addressables/ATagParser.kt | 123 ++ .../nip01Core/tags/addressables/Address.kt | 70 + .../tags/addressables/AddressReference.kt | 36 + .../tags/addressables/TagArrayBuilderExt.kt | 34 + .../tags/addressables/TagArrayExt.kt | 36 +- .../tags/dTags/TagArrayBuilderExt.kt | 3 +- .../quartz/nip01Core/tags/events/ETag.kt | 36 +- .../nip01Core/tags/events/EventReference.kt | 29 + .../nip01Core/tags/events/GenericETag.kt | 31 + .../tags/events/TagArrayBuilderExt.kt | 32 + .../nip01Core/tags/events/TagArrayExt.kt | 10 +- .../quartz/nip01Core/tags/geohash/GeoHash.kt | 19 +- .../tags/geohash/TagArrayBuilderExt.kt | 26 + .../nip01Core/tags/geohash/TagArrayExt.kt | 8 +- .../quartz/nip01Core/tags/hashtags/HashTag.kt | 62 + .../{HashTags.kt => TagArrayBuilderExt.kt} | 17 +- .../nip01Core/tags/hashtags/TagArrayExt.kt | 19 +- .../quartz/nip01Core/tags/kinds/EventExt.kt | 36 + .../quartz/nip01Core/tags/kinds/KindTag.kt | 52 + .../tags/kinds/TagArrayBuilderExt.kt | 30 + .../nip01Core/tags/kinds/TagArrayExt.kt | 39 + .../quartz/nip01Core/tags/people/EntityExt.kt | 32 + .../quartz/nip01Core/tags/people/EventExt.kt | 19 +- .../quartz/nip01Core/tags/people/PTag.kt | 79 + .../tags/people/PubKeyReferenceTag.kt} | 11 +- .../tags/people/TagArrayBuilderExt.kt | 38 + .../nip01Core/tags/people/TagArrayExt.kt | 17 +- .../nip01Core/tags/references/EventExt.kt | 38 + .../nip01Core/tags/references/ReferenceTag.kt | 40 + .../tags/references/TagArrayBuilderExt.kt} | 15 +- .../nip01Core/tags/references/TagArrayExt.kt | 45 + .../nip02FollowList/ContactListEvent.kt | 126 +- .../quartz/nip02FollowList/ContactTag.kt | 98 ++ .../nip02FollowList/ImmutableListOfLists.kt | 32 + .../quartz/nip02FollowList/RelaySet.kt | 48 + .../quartz/nip03Timestamp/OtsEvent.kt | 163 +- .../quartz/nip03Timestamp/OtsResolver.kt | 101 ++ .../nip03Timestamp/VerificationStateCache.kt | 44 + .../quartz/nip04Dm/Nip04Encoder.kt | 78 + .../nip04Dm/{Nip04.kt => Nip04Encryption.kt} | 76 +- .../quartz/nip04Dm/PrivateDmEvent.kt | 86 +- .../quartz/nip04Dm/TagArrayBuilderExt.kt | 30 + .../quartz/nip09Deletions/DeletionEvent.kt | 73 +- ...eTextNoteEvent.kt => BaseThreadedEvent.kt} | 58 +- .../quartz/nip10Notes/TextNoteEvent.kt | 150 +- .../quartz/nip10Notes/content/IndexedTags.kt | 4 + .../quartz/nip10Notes/content/NostrUris.kt | 25 + .../quartz/nip10Notes/tags/MarkedETag.kt | 251 +++ .../quartz/nip10Notes/tags/Positional.kt | 78 + .../quartz/nip10Notes/tags/ReplyBuilder.kt | 94 + .../nip10Notes/tags/TagArrayBuilderExt.kt | 34 + .../vitorpamplona/quartz/nip13Pow/PoWTag.kt | 22 +- .../quartz/nip13Pow/TagArrayBuilderExt.kt | 10 +- .../quartz/nip13Pow/TagArrayExt.kt | 8 +- .../quartz/nip14Subject/SubjectTag.kt | 22 +- .../quartz/nip14Subject/TagArrayBuilderExt.kt | 3 +- .../quartz/nip14Subject/TagArrayExt.kt | 9 +- .../ChatMessageEncryptedFileHeaderEvent.kt | 191 --- .../quartz/nip17Dm/ChatMessageEvent.kt | 124 -- .../quartz/nip17Dm/NIP17Factory.kt | 127 +- .../quartz/nip17Dm/base/BaseDMGroupEvent.kt | 68 + .../quartz/nip17Dm/{ => base}/ChatroomKey.kt | 2 +- .../nip17Dm/{ => base}/ChatroomKeyable.kt | 2 +- .../quartz/nip17Dm/{ => base}/NIP17Group.kt | 2 +- .../ChatMessageEncryptedFileHeaderEvent.kt | 118 ++ .../nip17Dm/files/TagArrayBuilderExt.kt | 86 + .../nip17Dm/{ => files/encryption}/AESGCM.kt | 2 +- .../{ => files/encryption}/NostrCipher.kt | 2 +- .../nip17Dm/files/tags/EncryptionAlgo.kt | 40 + .../nip17Dm/files/tags/EncryptionKey.kt | 43 + .../nip17Dm/files/tags/EncryptionNonce.kt | 43 + .../quartz/nip17Dm/files/tags/FileTypeTag.kt | 42 + .../messages/ChatMessageEvent.kt} | 82 +- .../nip17Dm/messages/TagArrayBuilderExt.kt | 39 + .../ChatMessageRelayListEvent.kt | 6 +- .../quartz/nip18Reposts/GenericRepostEvent.kt | 34 +- .../quartz/nip18Reposts/RepostEvent.kt | 36 +- .../quartz/nip18Reposts/quotes/EntityExt.kt | 53 + .../quartz/nip18Reposts/quotes/EventExt.kt | 36 + .../nip18Reposts/quotes/QAddressableTag.kt | 83 + .../quartz/nip18Reposts/quotes/QEventTag.kt | 65 + .../quartz/nip18Reposts/quotes/QTag.kt | 40 + .../nip18Reposts/quotes/TagArrayBuilderExt.kt | 51 + .../quartz/nip18Reposts/quotes/TagArrayExt.kt | 40 + .../quartz/nip19Bech32/ATagExt.kt | 2 +- .../quartz/nip19Bech32/Nip19Parser.kt | 2 +- .../quartz/nip19Bech32/entities/NProfile.kt | 4 +- .../quartz/nip21UriScheme/EventExt.kt | 3 + .../quartz/nip22Comments/CommentEvent.kt | 240 +-- .../nip22Comments/TagArrayBuilderExt.kt | 90 + .../nip22Comments/tags/ReplyAddressTag.kt | 70 + .../nip22Comments/tags/ReplyAuthorTag.kt | 64 + .../nip22Comments/tags/ReplyEventTag.kt | 84 + .../nip22Comments/tags/ReplyIdentifierTag.kt | 57 + .../quartz/nip22Comments/tags/ReplyKindTag.kt | 70 + .../nip22Comments/tags/RootAddressTag.kt | 88 + .../nip22Comments/tags/RootAuthorTag.kt | 64 + .../quartz/nip22Comments/tags/RootEventTag.kt | 84 + .../nip22Comments/tags/RootIdentifierTag.kt | 57 + .../quartz/nip22Comments/tags/RootKindTag.kt | 54 + .../nip23LongContent/LongTextNoteEvent.kt | 64 +- .../nip23LongContent/TagArrayBuilderExt.kt | 35 + .../quartz/nip23LongContent/tags/ImageTag.kt | 37 + .../nip23LongContent/tags/PublishedAtTag.kt | 37 + .../nip23LongContent/tags/SummaryTag.kt | 37 + .../quartz/nip23LongContent/tags/TitleTag.kt | 37 + .../quartz/nip25Reactions/ReactionEvent.kt | 88 +- .../nip28PublicChat/ChannelCreateEvent.kt | 103 -- .../nip28PublicChat/ChannelListEvent.kt | 4 +- .../nip28PublicChat/ChannelMessageEvent.kt | 115 -- .../nip28PublicChat/ChannelMetadataEvent.kt | 100 -- .../admin/ChannelCreateEvent.kt} | 50 +- .../{ => admin}/ChannelHideMessageEvent.kt | 44 +- .../admin/ChannelMetadataEvent.kt | 92 + .../{ => admin}/ChannelMuteUserEvent.kt | 45 +- .../base/BasePublicChatEvent.kt | 46 + .../nip28PublicChat/base/ChannelData.kt | 48 + .../base/IsInPublicChatChannel.kt | 30 + .../base/TagArrayBuilderExt.kt | 43 + .../message/ChannelMessageEvent.kt | 89 + .../quartz/nip30CustomEmoji/CustomEmoji.kt | 4 +- .../quartz/nip30CustomEmoji/EmojiUrlTag.kt | 55 + .../quartz/nip30CustomEmoji/EventExt.kt | 3 +- .../nip30CustomEmoji/TagArrayBuilderExt.kt | 28 + .../quartz/nip30CustomEmoji/TagArrayExt.kt | 26 + .../pack/EmojiPackEvent.kt} | 42 +- .../selection/EmojiPackSelectionEvent.kt | 85 + .../selection/TagArrayBuilderExt.kt | 33 + .../vitorpamplona/quartz/nip31Alts/AltTag.kt | 37 + .../quartz/nip31Alts/EventExt.kt | 5 +- .../quartz/nip31Alts/TagArrayBuilderExt.kt | 26 + .../{AltTagSerializer.kt => TagArrayExt.kt} | 9 +- .../quartz/nip34Git/GitReplyEvent.kt | 175 -- .../nip34Git/{ => issue}/GitIssueEvent.kt | 66 +- .../nip34Git/issue/TagArrayBuilderExt.kt | 37 + .../nip34Git/{ => patch}/GitPatchEvent.kt | 7 +- .../quartz/nip34Git/reply/GitReplyEvent.kt | 93 + .../nip34Git/reply/TagArrayBuilderExt.kt | 41 + .../{ => repository}/GitRepositoryEvent.kt | 44 +- .../nip34Git/repository/TagArrayBuilderExt.kt | 35 + .../nip34Git/repository/tags/CloneTag.kt | 37 + .../repository/tags/DescriptionTag.kt | 37 + .../nip34Git/repository/tags/NameTag.kt | 37 + .../quartz/nip34Git/repository/tags/WebTag.kt | 37 + .../quartz/nip35Torrents/DefaultTrackers.kt | 31 + .../nip35Torrents/TagArrayBuilderExt.kt | 42 + .../nip35Torrents/TorrentCommentEvent.kt | 137 +- .../quartz/nip35Torrents/TorrentEvent.kt | 99 +- .../quartz/nip35Torrents/tags/BtihTag.kt | 37 + .../quartz/nip35Torrents/tags/FileTag.kt | 55 + .../quartz/nip35Torrents/tags/InfoHashTag.kt | 37 + .../quartz/nip35Torrents/tags/TrackerTag.kt | 37 + .../ContentWarningTag.kt | 45 + .../quartz/nip36SensitiveContent/EventExt.kt | 10 +- .../TagArrayBuilderExt.kt | 26 + .../nip36SensitiveContent/TagArrayExt.kt | 31 + .../quartz/nip37Drafts/DraftEvent.kt | 10 +- .../quartz/nip39ExtIdentities/EventExt.kt | 2 +- .../quartz/nip44Encryption/Nip44.kt | 30 +- .../BunkerResponseGetRelays.kt | 2 +- .../nip46RemoteSigner/NostrConnectEvent.kt | 4 +- .../LnZapPaymentRequestEvent.kt | 4 +- .../nip48ProxyTags/TagArrayBuilderExt.kt | 7 +- .../nip50Search/SearchRelayListEvent.kt | 4 +- .../quartz/nip51Lists/BookmarkListEvent.kt | 6 +- .../quartz/nip51Lists/GeneralListEvent.kt | 3 + .../quartz/nip51Lists/MuteListEvent.kt | 64 +- .../quartz/nip51Lists/Nip51PrivateTags.kt | 63 + .../quartz/nip51Lists/PeopleListEvent.kt | 64 +- .../quartz/nip51Lists/PinListEvent.kt | 4 +- .../quartz/nip51Lists/PrivateTagArrayEvent.kt | 32 +- .../quartz/nip51Lists/RelaySetEvent.kt | 4 +- .../quartz/nip51Lists/TagArrayExt.kt | 4 +- .../nip52Calendar/CalendarDateSlotEvent.kt | 4 +- .../quartz/nip52Calendar/CalendarEvent.kt | 4 +- .../quartz/nip52Calendar/CalendarRSVPEvent.kt | 4 +- .../nip52Calendar/CalendarTimeSlotEvent.kt | 4 +- .../LiveActivitiesChatMessageEvent.kt | 114 -- .../LiveActivitiesEvent.kt | 93 - .../chat/LiveActivitiesChatMessageEvent.kt | 85 + .../chat/TagArrayBuilderExt.kt | 40 + .../streaming/LiveActivitiesEvent.kt | 103 ++ .../streaming/tags/CurrentParticipantsTag.kt | 37 + .../streaming/tags/EndsTag.kt | 37 + .../streaming/tags/ParticipantTag.kt | 91 + .../streaming/tags/RelayListTag.kt | 46 + .../streaming/tags/StartsTag.kt | 37 + .../streaming/tags/StatusTag.kt | 48 + .../streaming/tags/StreamingTag.kt | 37 + .../streaming/tags/TotalParticipantsTag.kt | 37 + .../quartz/nip54Wiki/WikiNoteEvent.kt | 16 +- .../quartz/nip56Reports/ReportEvent.kt | 8 +- .../quartz/nip57Zaps/LnZapEvent.kt | 4 +- .../quartz/nip57Zaps/LnZapRequestEvent.kt | 12 +- .../nip57Zaps/PrivateZapRequestBuilder.kt | 2 +- .../nip57Zaps/splits/BaseZapSplitSetup.kt | 4 + .../quartz/nip57Zaps/splits/EventExt.kt | 6 +- .../nip57Zaps/splits/TagArrayBuilderExt.kt | 30 + .../quartz/nip57Zaps/splits/TagArrayExt.kt | 29 + .../nip57Zaps/splits/ZapSplitSetupParser.kt | 2 +- .../splits/ZapSplitSetupSerializer.kt | 4 +- .../quartz/nip57Zaps/zapraiser/EventExt.kt | 3 +- .../nip57Zaps/zapraiser/TagArrayBuilderExt.kt | 26 + ...{ZapRaiserSerializer.kt => TagArrayExt.kt} | 10 +- .../nip57Zaps/zapraiser/ZapRaiserTag.kt | 45 + .../quartz/nip58Badges/BadgeAwardEvent.kt | 3 + .../nip59Giftwrap/{ => rumors}/Rumor.kt | 3 +- .../{ => rumors}/RumorDeserializer.kt | 2 +- .../{ => rumors}/RumorSerializer.kt | 2 +- .../{ => seals}/SealedRumorEvent.kt | 5 +- .../{ => wraps}/GiftWrapEvent.kt | 6 +- .../AdvertisedRelayListEvent.kt | 4 +- .../quartz/nip68Picture/IMetaTagBuilderExt.kt | 75 + .../quartz/nip68Picture/IMetaTagExt.kt | 74 + .../quartz/nip68Picture/PictureEvent.kt | 363 +--- .../quartz/nip68Picture/PictureMeta.kt | 70 + .../quartz/nip68Picture/TagArrayBuilderExt.kt | 54 + .../nip68Picture/tags/UserAnnotationTag.kt | 52 + .../quartz/nip71Video/IMetaTagBuilderExt.kt | 72 + .../quartz/nip71Video/IMetaTagExt.kt | 71 + .../quartz/nip71Video/TagArrayBuilderExt.kt | 62 + .../quartz/nip71Video/VideoEvent.kt | 230 +-- .../quartz/nip71Video/VideoHorizontalEvent.kt | 67 +- .../quartz/nip71Video/VideoMeta.kt | 69 + .../quartz/nip71Video/VideoVerticalEvent.kt | 56 +- .../quartz/nip71Video/VideoViewEvent.kt | 82 - .../quartz/nip71Video/tags/DurationTag.kt | 37 + .../quartz/nip71Video/tags/SegmentTag.kt | 51 + .../tags/TextTrackTag.kt} | 46 +- .../nip72ModCommunities/CommunityListEvent.kt | 4 +- .../CommunityPostApprovalEvent.kt | 66 +- .../approval/TagArrayBuilderExt.kt | 40 + .../definition/CommunityDefinitionEvent.kt | 91 + .../definition/TagArrayBuilderExt.kt | 41 + .../definition/tags/DescriptionTag.kt | 37 + .../definition/tags/ImageTag.kt | 48 + .../definition/tags/ModeratorTag.kt | 63 + .../definition/tags/NameTag.kt | 37 + .../definition/tags/RelayTag.kt | 47 + .../definition/tags/RulesTag.kt | 37 + .../quartz/nip75ZapGoals/GoalEvent.kt | 74 +- .../nip75ZapGoals/TagArrayBuilderExt.kt | 44 + .../quartz/nip75ZapGoals/tags/AmountTag.kt | 37 + .../quartz/nip75ZapGoals/tags/ClosedAtTag.kt | 37 + .../quartz/nip75ZapGoals/tags/RelayListTag.kt | 46 + .../nip78AppData/AppSpecificDataEvent.kt | 4 +- .../quartz/nip84Highlights/HighlightEvent.kt | 15 +- .../quartz/nip84Highlights/tags/ContextTag.kt | 37 + .../quartz/nip89AppHandlers/PlatformType.kt | 29 + .../{ => definition}/AppDefinitionEvent.kt | 68 +- .../{ => definition}/AppMetadata.kt | 21 +- .../nip89AppHandlers/definition/EntityType.kt | 31 + .../definition/TagArrayBuilderExt.kt | 26 + .../definition/tags/PlatformLinkTag.kt | 60 + .../recommendation/AppRecommendationEvent.kt} | 50 +- .../recommendation/TagArrayBuilderExt.kt | 41 + .../recommendation/tags/RecommendationTag.kt | 69 + .../NIP90ContentDiscoveryRequestEvent.kt | 4 +- .../NIP90ContentDiscoveryResponseEvent.kt | 6 +- .../quartz/nip90Dvms/NIP90StatusEvent.kt | 6 +- .../NIP90UserDiscoveryRequestEvent.kt | 6 +- .../NIP90UserDiscoveryResponseEvent.kt | 6 +- .../TorrentFile.kt => nip92IMeta/EventExt.kt} | 10 +- .../quartz/nip92IMeta/IMetaTag.kt | 50 +- .../quartz/nip92IMeta/IMetaTagBuilder.kt | 37 +- .../nip92IMeta/Nip92MediaAttachments.kt | 76 - .../quartz/nip92IMeta/TagArrayBuilderExt.kt | 28 + .../quartz/nip92IMeta/TagArrayExt.kt | 27 + .../nip94FileMetadata/FileHeaderEvent.kt | 152 +- .../nip94FileMetadata/IMetaTagBuilderExt.kt | 72 + .../nip94FileMetadata/TagArrayBuilderExt.kt | 66 + .../nip94FileMetadata/tags/BlurhashTag.kt | 37 + .../{Dimension.kt => tags/DimensionTag.kt} | 23 +- .../nip94FileMetadata/tags/FallbackTag.kt | 37 + .../quartz/nip94FileMetadata/tags/HashTag.kt | 37 + .../quartz/nip94FileMetadata/tags/ImageTag.kt | 46 + .../nip94FileMetadata/tags/MagnetTag.kt | 37 + .../nip94FileMetadata/tags/MimeTypeTag.kt | 42 + .../nip94FileMetadata/tags/OriginalHashTag.kt | 37 + .../nip94FileMetadata/tags/ServiceTag.kt | 40 + .../quartz/nip94FileMetadata/tags/SizeTag.kt | 37 + .../nip94FileMetadata/tags/SummaryTag.kt | 40 + .../quartz/nip94FileMetadata/tags/ThumbTag.kt | 46 + .../nip94FileMetadata/tags/TorrentInfoHash.kt | 37 + .../quartz/nip94FileMetadata/tags/UrlTag.kt | 40 + .../DeleteResult.kt} | 20 +- .../UploadResult.kt} | 20 +- .../{ => config}/FileServersEvent.kt | 72 +- .../config/TagArrayBuilderExt.kt | 28 + .../nip96FileStorage/config/tags/ServerTag.kt | 42 + .../nip96FileStorage/{ => info}/ServerInfo.kt | 2 +- .../{ => info}/ServerInfoParser.kt | 2 +- .../nip98HttpAuth/HTTPAuthorizationEvent.kt | 41 +- .../nip98HttpAuth/TagArrayBuilderExt.kt | 34 + .../quartz/nip98HttpAuth/tags/MethodTag.kt | 37 + .../nip98HttpAuth/tags/PayloadHashTag.kt | 44 + .../quartz/nip98HttpAuth/tags/UrlTag.kt | 37 + .../nip99Classifieds/ClassifiedsEvent.kt | 204 +-- .../nip99Classifieds/TagArrayBuilderExt.kt | 49 + .../nip99Classifieds/tags/ConditionTag.kt | 49 + .../nip99Classifieds/tags/LocationTag.kt | 37 + .../quartz/nip99Classifieds/tags/PriceTag.kt | 49 + .../quartz/nip99Classifieds/tags/StatusTag.kt | 47 + .../vitorpamplona/quartz/utils/ArrayUtils.kt | 12 + .../vitorpamplona/quartz/utils/StringUtils.kt | 4 + .../vitorpamplona/quartz/utils/TimeUtils.kt | 2 +- .../quartz/utils/UriReferenceExt.kt | 43 + .../quartz/nip96FileStorage/Nip96Test.kt | 1 + .../quartz/utils/HexEncodingTest.kt | 80 + .../{TimeUtilsTest.kt => StringUtilsTest.kt} | 2 +- 614 files changed, 15380 insertions(+), 7132 deletions(-) rename quartz/src/androidTest/java/com/vitorpamplona/quartz/nip04Dm/{Nip04Test.kt => Nip04EncryptionTest.kt} (79%) create mode 100644 quartz/src/androidTest/java/com/vitorpamplona/quartz/nip10Notes/ThreadingTests.kt delete mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/AudioHeaderEvent.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/header/AudioHeaderEvent.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/header/TagArrayBuilderExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/header/tags/DownloadUrlTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/header/tags/StreamUrlTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/header/tags/WaveformTag.kt rename quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/{ => track}/AudioTrackEvent.kt (54%) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/track/TagArrayBuilderExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/track/tags/CoverTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/track/tags/MediaTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/track/tags/ParticipantTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/track/tags/PriceTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/track/tags/TypeTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/experimental/bounties/BountyAddValueEvent.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/experimental/bounties/RewardTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/experimental/bounties/TagArrayExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/experimental/forks/BaseThreadedEventExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/experimental/forks/MarkedETagExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/experimental/interactiveStories/TagArrayBuilderExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/experimental/interactiveStories/tags/ReadStatusTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/experimental/interactiveStories/tags/RootSceneTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/experimental/interactiveStories/tags/StoryOptionTag.kt delete mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/experimental/nip95/FileStorageHeaderEvent.kt rename quartz/src/main/java/com/vitorpamplona/quartz/experimental/nip95/{ => data}/FileStorageEvent.kt (64%) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/experimental/nip95/data/TagArrayBuilderExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/experimental/nip95/header/FileStorageHeaderEvent.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/experimental/nip95/header/TagArrayBuilderExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/experimental/nns/TagArrayBuilderExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/experimental/nns/tags/IPv4Tag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/experimental/nns/tags/IPv6Tag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/experimental/nns/tags/VersionTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/experimental/profileGallery/TagArrayBuilderExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/experimental/relationshipStatus/TagArrayBuilderExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/experimental/relationshipStatus/tags/PetnameTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/experimental/relationshipStatus/tags/RankTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/experimental/relationshipStatus/tags/SummaryTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/experimental/zapPolls/TagArrayBuilderExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/experimental/zapPolls/tags/ClosedAtTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/experimental/zapPolls/tags/ConsensusThresholdTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/experimental/zapPolls/tags/MaximumTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/experimental/zapPolls/tags/MinimumTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/experimental/zapPolls/tags/PollOptionTag.kt rename quartz/src/main/java/com/vitorpamplona/quartz/{nip99Classifieds/Price.kt => nip01Core/core/IEvent.kt} (88%) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/Tag.kt rename quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/{ => crypto}/KeyPair.kt (95%) rename quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/{ => crypto}/Nip01.kt (98%) rename quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/{ => metadata}/MetadataEvent.kt (93%) rename quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/{ => metadata}/UserMetadata.kt (98%) rename quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/{relays => relay}/RelayStat.kt (98%) rename quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/{relays => relay}/RelayState.kt (96%) rename quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/{relays => relay}/SimpleClientRelay.kt (92%) rename quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/{relays => relay}/Subscription.kt (91%) rename quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/{relays => relay}/SubscriptionCollection.kt (92%) rename quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/{relays => relay}/commands/toClient/AuthMessage.kt (95%) rename quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/{relays => relay}/commands/toClient/ClosedMessage.kt (95%) rename quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/{relays => relay}/commands/toClient/EoseMessage.kt (95%) rename quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/{relays => relay}/commands/toClient/EventMessage.kt (96%) rename quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/{relays => relay}/commands/toClient/Message.kt (94%) rename quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/{relays => relay}/commands/toClient/NoticeMessage.kt (95%) rename quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/{relays => relay}/commands/toClient/NotifyMessage.kt (95%) rename quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/{relays => relay}/commands/toClient/OkMessage.kt (96%) rename quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/{relays => relay}/commands/toClient/ToClientParser.kt (96%) rename quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/{relays => relay}/commands/toRelay/AuthCmd.kt (96%) rename quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/{relays => relay}/commands/toRelay/CloseCmd.kt (95%) rename quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/{relays => relay}/commands/toRelay/Command.kt (94%) rename quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/{relays => relay}/commands/toRelay/CountCmd.kt (91%) rename quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/{relays => relay}/commands/toRelay/EventCmd.kt (96%) rename quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/{relays => relay}/commands/toRelay/ReqCmd.kt (85%) rename quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/{relays => relay}/commands/toRelay/ToRelayParser.kt (96%) rename quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/{relays => relay}/filters/Filter.kt (97%) rename quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/{relays => relay}/filters/FilterDeserializer.kt (97%) rename quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/{relays => relay}/filters/FilterMatcher.kt (97%) rename quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/{relays => relay}/filters/FilterSerializer.kt (98%) rename quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/{relays => relay}/sockets/WebSocket.kt (95%) rename quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/{relays => relay}/sockets/WebSocketListener.kt (96%) rename quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/{relays => relay}/sockets/WebsocketBuilder.kt (95%) rename quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/{relays => relay}/sockets/WebsocketBuilderFactory.kt (95%) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/EventTemplate.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/ATagParser.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/Address.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/AddressReference.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/TagArrayBuilderExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/events/EventReference.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/events/GenericETag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/events/TagArrayBuilderExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/geohash/TagArrayBuilderExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/hashtags/HashTag.kt rename quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/hashtags/{HashTags.kt => TagArrayBuilderExt.kt} (76%) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/kinds/EventExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/kinds/KindTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/kinds/TagArrayBuilderExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/kinds/TagArrayExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/people/EntityExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/people/PTag.kt rename quartz/src/main/java/com/vitorpamplona/quartz/{nip36SensitiveContent/ContentWarningSerializer.kt => nip01Core/tags/people/PubKeyReferenceTag.kt} (85%) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/people/TagArrayBuilderExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/references/EventExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/references/ReferenceTag.kt rename quartz/src/main/java/com/vitorpamplona/quartz/{nip96FileStorage/AuthToken.kt => nip01Core/tags/references/TagArrayBuilderExt.kt} (75%) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/references/TagArrayExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/ContactTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/ImmutableListOfLists.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/RelaySet.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/OtsResolver.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/VerificationStateCache.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/Nip04Encoder.kt rename quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/{Nip04.kt => Nip04Encryption.kt} (60%) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/TagArrayBuilderExt.kt rename quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/{BaseTextNoteEvent.kt => BaseThreadedEvent.kt} (74%) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/content/NostrUris.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/tags/MarkedETag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/tags/Positional.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/tags/ReplyBuilder.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/tags/TagArrayBuilderExt.kt delete mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/ChatMessageEncryptedFileHeaderEvent.kt delete mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/ChatMessageEvent.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/base/BaseDMGroupEvent.kt rename quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/{ => base}/ChatroomKey.kt (96%) rename quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/{ => base}/ChatroomKeyable.kt (96%) rename quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/{ => base}/NIP17Group.kt (96%) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/files/ChatMessageEncryptedFileHeaderEvent.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/files/TagArrayBuilderExt.kt rename quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/{ => files/encryption}/AESGCM.kt (97%) rename quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/{ => files/encryption}/NostrCipher.kt (95%) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/files/tags/EncryptionAlgo.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/files/tags/EncryptionKey.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/files/tags/EncryptionNonce.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/files/tags/FileTypeTag.kt rename quartz/src/main/java/com/vitorpamplona/quartz/{nip30CustomEmoji/EmojiPackEvent.kt => nip17Dm/messages/ChatMessageEvent.kt} (50%) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/messages/TagArrayBuilderExt.kt rename quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/{ => settings}/ChatMessageRelayListEvent.kt (95%) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/EntityExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/EventExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/QAddressableTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/QEventTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/QTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/TagArrayBuilderExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/TagArrayExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/TagArrayBuilderExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/ReplyAddressTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/ReplyAuthorTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/ReplyEventTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/ReplyIdentifierTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/ReplyKindTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/RootAddressTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/RootAuthorTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/RootEventTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/RootIdentifierTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/RootKindTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip23LongContent/TagArrayBuilderExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip23LongContent/tags/ImageTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip23LongContent/tags/PublishedAtTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip23LongContent/tags/SummaryTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip23LongContent/tags/TitleTag.kt delete mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/ChannelCreateEvent.kt delete mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/ChannelMessageEvent.kt delete mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/ChannelMetadataEvent.kt rename quartz/src/main/java/com/vitorpamplona/quartz/{nip30CustomEmoji/EmojiPackSelectionEvent.kt => nip28PublicChat/admin/ChannelCreateEvent.kt} (55%) rename quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/{ => admin}/ChannelHideMessageEvent.kt (60%) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelMetadataEvent.kt rename quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/{ => admin}/ChannelMuteUserEvent.kt (60%) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/base/BasePublicChatEvent.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/base/ChannelData.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/base/IsInPublicChatChannel.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/base/TagArrayBuilderExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/message/ChannelMessageEvent.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip30CustomEmoji/EmojiUrlTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip30CustomEmoji/TagArrayBuilderExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip30CustomEmoji/TagArrayExt.kt rename quartz/src/main/java/com/vitorpamplona/quartz/{nip89AppHandlers/AppRecommendationEvent.kt => nip30CustomEmoji/pack/EmojiPackEvent.kt} (59%) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip30CustomEmoji/selection/EmojiPackSelectionEvent.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip30CustomEmoji/selection/TagArrayBuilderExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip31Alts/AltTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip31Alts/TagArrayBuilderExt.kt rename quartz/src/main/java/com/vitorpamplona/quartz/nip31Alts/{AltTagSerializer.kt => TagArrayExt.kt} (87%) delete mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/GitReplyEvent.kt rename quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/{ => issue}/GitIssueEvent.kt (52%) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/issue/TagArrayBuilderExt.kt rename quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/{ => patch}/GitPatchEvent.kt (94%) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/reply/GitReplyEvent.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/reply/TagArrayBuilderExt.kt rename quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/{ => repository}/GitRepositoryEvent.kt (53%) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/repository/TagArrayBuilderExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/repository/tags/CloneTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/repository/tags/DescriptionTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/repository/tags/NameTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/repository/tags/WebTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip35Torrents/DefaultTrackers.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip35Torrents/TagArrayBuilderExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip35Torrents/tags/BtihTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip35Torrents/tags/FileTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip35Torrents/tags/InfoHashTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip35Torrents/tags/TrackerTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip36SensitiveContent/ContentWarningTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip36SensitiveContent/TagArrayBuilderExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip36SensitiveContent/TagArrayExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/Nip51PrivateTags.kt delete mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/LiveActivitiesChatMessageEvent.kt delete mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/LiveActivitiesEvent.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/chat/LiveActivitiesChatMessageEvent.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/chat/TagArrayBuilderExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/LiveActivitiesEvent.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/tags/CurrentParticipantsTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/tags/EndsTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/tags/ParticipantTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/tags/RelayListTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/tags/StartsTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/tags/StatusTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/tags/StreamingTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/tags/TotalParticipantsTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/splits/TagArrayBuilderExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/splits/TagArrayExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/zapraiser/TagArrayBuilderExt.kt rename quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/zapraiser/{ZapRaiserSerializer.kt => TagArrayExt.kt} (85%) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/zapraiser/ZapRaiserTag.kt rename quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/{ => rumors}/Rumor.kt (95%) rename quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/{ => rumors}/RumorDeserializer.kt (97%) rename quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/{ => rumors}/RumorSerializer.kt (97%) rename quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/{ => seals}/SealedRumorEvent.kt (95%) rename quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/{ => wraps}/GiftWrapEvent.kt (95%) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip68Picture/IMetaTagBuilderExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip68Picture/IMetaTagExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip68Picture/PictureMeta.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip68Picture/TagArrayBuilderExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip68Picture/tags/UserAnnotationTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/IMetaTagBuilderExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/IMetaTagExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/TagArrayBuilderExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/VideoMeta.kt delete mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/VideoViewEvent.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/tags/DurationTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/tags/SegmentTag.kt rename quartz/src/main/java/com/vitorpamplona/quartz/{nip10Notes/PTag.kt => nip71Video/tags/TextTrackTag.kt} (57%) rename quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/{ => approval}/CommunityPostApprovalEvent.kt (53%) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/approval/TagArrayBuilderExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/definition/CommunityDefinitionEvent.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/definition/TagArrayBuilderExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/definition/tags/DescriptionTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/definition/tags/ImageTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/definition/tags/ModeratorTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/definition/tags/NameTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/definition/tags/RelayTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/definition/tags/RulesTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip75ZapGoals/TagArrayBuilderExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip75ZapGoals/tags/AmountTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip75ZapGoals/tags/ClosedAtTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip75ZapGoals/tags/RelayListTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip84Highlights/tags/ContextTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/PlatformType.kt rename quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/{ => definition}/AppDefinitionEvent.kt (57%) rename quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/{ => definition}/AppMetadata.kt (91%) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/definition/EntityType.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/definition/TagArrayBuilderExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/definition/tags/PlatformLinkTag.kt rename quartz/src/main/java/com/vitorpamplona/quartz/{nip72ModCommunities/CommunityDefinitionEvent.kt => nip89AppHandlers/recommendation/AppRecommendationEvent.kt} (52%) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/recommendation/TagArrayBuilderExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/recommendation/tags/RecommendationTag.kt rename quartz/src/main/java/com/vitorpamplona/quartz/{nip35Torrents/TorrentFile.kt => nip92IMeta/EventExt.kt} (86%) delete mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip92IMeta/Nip92MediaAttachments.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip92IMeta/TagArrayBuilderExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip92IMeta/TagArrayExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/IMetaTagBuilderExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/TagArrayBuilderExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/BlurhashTag.kt rename quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/{Dimension.kt => tags/DimensionTag.kt} (74%) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/FallbackTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/HashTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/ImageTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/MagnetTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/MimeTypeTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/OriginalHashTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/ServiceTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/SizeTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/SummaryTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/ThumbTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/TorrentInfoHash.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/UrlTag.kt rename quartz/src/main/java/com/vitorpamplona/quartz/nip96FileStorage/{ResultParser.kt => actions/DeleteResult.kt} (70%) rename quartz/src/main/java/com/vitorpamplona/quartz/nip96FileStorage/{Nip96Result.kt => actions/UploadResult.kt} (75%) rename quartz/src/main/java/com/vitorpamplona/quartz/nip96FileStorage/{ => config}/FileServersEvent.kt (57%) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip96FileStorage/config/TagArrayBuilderExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip96FileStorage/config/tags/ServerTag.kt rename quartz/src/main/java/com/vitorpamplona/quartz/nip96FileStorage/{ => info}/ServerInfo.kt (97%) rename quartz/src/main/java/com/vitorpamplona/quartz/nip96FileStorage/{ => info}/ServerInfoParser.kt (98%) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip98HttpAuth/TagArrayBuilderExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip98HttpAuth/tags/MethodTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip98HttpAuth/tags/PayloadHashTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip98HttpAuth/tags/UrlTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip99Classifieds/TagArrayBuilderExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip99Classifieds/tags/ConditionTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip99Classifieds/tags/LocationTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip99Classifieds/tags/PriceTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip99Classifieds/tags/StatusTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/utils/UriReferenceExt.kt create mode 100644 quartz/src/test/java/com/vitorpamplona/quartz/utils/HexEncodingTest.kt rename quartz/src/test/java/com/vitorpamplona/quartz/utils/{TimeUtilsTest.kt => StringUtilsTest.kt} (99%) diff --git a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ImageUploadTesting.kt b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ImageUploadTesting.kt index fe7f8b917..e8aaca567 100644 --- a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ImageUploadTesting.kt +++ b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ImageUploadTesting.kt @@ -35,7 +35,7 @@ import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType import com.vitorpamplona.quartz.CryptoUtils -import com.vitorpamplona.quartz.nip01Core.KeyPair +import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.toHexKey import junit.framework.TestCase.assertEquals import junit.framework.TestCase.fail diff --git a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/OkHttpOtsTest.kt b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/OkHttpOtsTest.kt index 915268552..956ae15ad 100644 --- a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/OkHttpOtsTest.kt +++ b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/OkHttpOtsTest.kt @@ -23,10 +23,11 @@ package com.vitorpamplona.amethyst import androidx.test.ext.junit.runners.AndroidJUnit4 import com.vitorpamplona.amethyst.service.ots.OkHttpBlockstreamExplorer import com.vitorpamplona.amethyst.service.ots.OkHttpCalendarBuilder -import com.vitorpamplona.quartz.nip01Core.KeyPair +import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent +import com.vitorpamplona.quartz.nip03Timestamp.OtsResolver import com.vitorpamplona.quartz.nip03Timestamp.ots.OpenTimestamps import junit.framework.TestCase.assertEquals import org.junit.Assert @@ -46,27 +47,27 @@ class OkHttpOtsTest { @Before fun setup() { - OtsEvent.otsInstance = OpenTimestamps(OkHttpBlockstreamExplorer(forceProxy = { false }), OkHttpCalendarBuilder(forceProxy = { false })) + OtsResolver.ots = OpenTimestamps(OkHttpBlockstreamExplorer(forceProxy = { false }), OkHttpCalendarBuilder(forceProxy = { false })) } @Test fun verifyNostrEvent() { val ots = EventMapper.fromJson(otsEvent) as OtsEvent - println(ots.info()) + println(OtsResolver.info(ots.otsByteArray())) assertEquals(1707688818L, ots.verify()) } @Test fun verifyNostrEvent2() { val ots = EventMapper.fromJson(otsEvent2) as OtsEvent - println(ots.info()) + println(OtsResolver.info(ots.otsByteArray())) assertEquals(1706322179L, ots.verify()) } @Test fun verifyNostrPendingEvent() { val ots = EventMapper.fromJson(otsPendingEvent) as OtsEvent - println(ots.info()) + println(OtsResolver.info(ots.otsByteArray())) assertEquals(null, ots.verify()) } @@ -79,7 +80,7 @@ class OkHttpOtsTest { val otsFile = OtsEvent.stamp(otsEvent2Digest) - OtsEvent.create(otsEvent2Digest, otsFile, signer) { + signer.sign(OtsEvent.build(otsEvent2Digest, otsFile)) { ots = it countDownLatch.countDown() } @@ -87,7 +88,7 @@ class OkHttpOtsTest { Assert.assertTrue(countDownLatch.await(1, TimeUnit.SECONDS)) println(ots!!.toJson()) - println(ots!!.info()) + println(OtsResolver.info(ots!!.otsByteArray())) // Should not be valid because we need to wait for confirmations assertEquals(null, ots!!.verify()) diff --git a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ThreadAssemblerTest.kt b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ThreadAssemblerTest.kt index 89ad1d142..c45397357 100644 --- a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ThreadAssemblerTest.kt +++ b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ThreadAssemblerTest.kt @@ -26,11 +26,11 @@ import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.ThreadFeedFilter -import com.vitorpamplona.quartz.nip01Core.KeyPair import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.hasValidSignature +import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip01Core.verify import junit.framework.TestCase import junit.framework.TestCase.assertEquals import kotlinx.coroutines.CoroutineScope @@ -125,7 +125,7 @@ class ThreadAssemblerTest { var counter = 0 eventArray.forEach { - TestCase.assertTrue("${it.id} failed signature check", it.hasValidSignature()) + TestCase.assertTrue("${it.id} failed signature check", it.verify()) LocalCache.verifyAndConsume(it, null) counter++ } diff --git a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/UrlUserTagTransformationTest.kt b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/UrlUserTagTransformationTest.kt index 8babb8e31..cdbab488b 100644 --- a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/UrlUserTagTransformationTest.kt +++ b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/UrlUserTagTransformationTest.kt @@ -27,7 +27,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.actions.buildAnnotatedStringWithUrlHighlighting -import com.vitorpamplona.quartz.nip01Core.UserMetadata +import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata import com.vitorpamplona.quartz.nip01Core.toHexKey import com.vitorpamplona.quartz.nip19Bech32.decodePublicKey import org.junit.Assert.assertEquals diff --git a/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushMessageReceiver.kt b/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushMessageReceiver.kt index 01092dcf1..20d697e8b 100644 --- a/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushMessageReceiver.kt +++ b/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushMessageReceiver.kt @@ -30,7 +30,7 @@ import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.getOrCreateDMChannel import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.getOrCreateZapChannel import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip59Giftwrap.GiftWrapEvent +import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index 7882749ac..514a8d696 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -46,14 +46,14 @@ import com.vitorpamplona.amethyst.ui.tor.TorType import com.vitorpamplona.ammolite.relays.RelaySetupInfo import com.vitorpamplona.quartz.experimental.edits.PrivateOutboxRelayListEvent import com.vitorpamplona.quartz.nip01Core.HexKey -import com.vitorpamplona.quartz.nip01Core.KeyPair -import com.vitorpamplona.quartz.nip01Core.MetadataEvent import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.hexToByteArray import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper +import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip01Core.toHexKey import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent -import com.vitorpamplona.quartz.nip17Dm.ChatMessageRelayListEvent +import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent import com.vitorpamplona.quartz.nip19Bech32.toNpub import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt index 7d20c24da..25091ccb3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt @@ -58,7 +58,7 @@ import com.vitorpamplona.amethyst.ui.tor.TorManager import com.vitorpamplona.amethyst.ui.tor.TorType import com.vitorpamplona.ammolite.relays.NostrClient import com.vitorpamplona.quartz.nip01Core.toHexKey -import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent +import com.vitorpamplona.quartz.nip03Timestamp.OtsResolver import com.vitorpamplona.quartz.nip03Timestamp.ots.OpenTimestamps import com.vitorpamplona.quartz.nip19Bech32.bech32.bechToBytes import com.vitorpamplona.quartz.nip19Bech32.decodePublicKeyAsHexOrNull @@ -110,13 +110,13 @@ class ServiceManager( else -> HttpClientManager.setDefaultProxy(null) } - OtsEvent.otsInstance = + OtsResolver.ots = OpenTimestamps( OkHttpBlockstreamExplorer(myAccount::shouldUseTorForMoneyOperations), OkHttpCalendarBuilder(myAccount::shouldUseTorForMoneyOperations), ) } else { - OtsEvent.otsInstance = + OtsResolver.ots = OpenTimestamps( OkHttpBlockstreamExplorer { false }, OkHttpCalendarBuilder { false }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index b300173e3..d6091f0a1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -52,58 +52,87 @@ import com.vitorpamplona.ammolite.relays.TypedFilter import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter import com.vitorpamplona.quartz.blossom.BlossomAuthorizationEvent import com.vitorpamplona.quartz.blossom.BlossomServersEvent +import com.vitorpamplona.quartz.experimental.bounties.BountyAddValueEvent import com.vitorpamplona.quartz.experimental.edits.PrivateOutboxRelayListEvent import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryBaseEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryReadingStateEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStorySceneEvent -import com.vitorpamplona.quartz.experimental.interactiveStories.StoryOption -import com.vitorpamplona.quartz.experimental.nip95.FileStorageEvent -import com.vitorpamplona.quartz.experimental.nip95.FileStorageHeaderEvent +import com.vitorpamplona.quartz.experimental.interactiveStories.image +import com.vitorpamplona.quartz.experimental.interactiveStories.summary +import com.vitorpamplona.quartz.experimental.interactiveStories.tags.StoryOptionTag +import com.vitorpamplona.quartz.experimental.nip95.data.FileStorageEvent +import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent +import com.vitorpamplona.quartz.experimental.nip95.header.blurhash +import com.vitorpamplona.quartz.experimental.nip95.header.dimension +import com.vitorpamplona.quartz.experimental.nip95.header.fileSize +import com.vitorpamplona.quartz.experimental.nip95.header.hash +import com.vitorpamplona.quartz.experimental.nip95.header.mimeType import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent -import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent +import com.vitorpamplona.quartz.experimental.profileGallery.blurhash +import com.vitorpamplona.quartz.experimental.profileGallery.dimension +import com.vitorpamplona.quartz.experimental.profileGallery.fromEvent +import com.vitorpamplona.quartz.experimental.profileGallery.hash +import com.vitorpamplona.quartz.experimental.profileGallery.mimeType import com.vitorpamplona.quartz.nip01Core.EventHintBundle import com.vitorpamplona.quartz.nip01Core.HexKey -import com.vitorpamplona.quartz.nip01Core.KeyPair -import com.vitorpamplona.quartz.nip01Core.MetadataEvent import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.hexToByteArray import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper +import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent +import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedAddresses -import com.vitorpamplona.quartz.nip01Core.tags.events.ETag import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEventIds +import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohash import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohashes import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip01Core.tags.people.hasAnyTaggedUser import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser -import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUsers -import com.vitorpamplona.quartz.nip02FollowList.Contact +import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUserIds +import com.vitorpamplona.quartz.nip01Core.tags.references.references import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent +import com.vitorpamplona.quartz.nip02FollowList.ContactTag +import com.vitorpamplona.quartz.nip02FollowList.ReadWrite import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent import com.vitorpamplona.quartz.nip04Dm.PrivateDmEvent +import com.vitorpamplona.quartz.nip04Dm.reply import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent -import com.vitorpamplona.quartz.nip10Notes.PTag import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent -import com.vitorpamplona.quartz.nip17Dm.ChatMessageRelayListEvent +import com.vitorpamplona.quartz.nip10Notes.content.findHashtags +import com.vitorpamplona.quartz.nip10Notes.content.findNostrUris +import com.vitorpamplona.quartz.nip10Notes.content.findURLs import com.vitorpamplona.quartz.nip17Dm.NIP17Factory -import com.vitorpamplona.quartz.nip17Dm.NIP17Group +import com.vitorpamplona.quartz.nip17Dm.base.NIP17Group +import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent +import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent +import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent -import com.vitorpamplona.quartz.nip22Comments.CommentEvent +import com.vitorpamplona.quartz.nip18Reposts.quotes.quotes +import com.vitorpamplona.quartz.nip19Bech32.entities.Entity +import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress +import com.vitorpamplona.quartz.nip19Bech32.entities.NEmbed +import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent +import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile +import com.vitorpamplona.quartz.nip19Bech32.entities.NPub +import com.vitorpamplona.quartz.nip19Bech32.entities.NRelay +import com.vitorpamplona.quartz.nip19Bech32.entities.NSec import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent -import com.vitorpamplona.quartz.nip28PublicChat.ChannelCreateEvent -import com.vitorpamplona.quartz.nip28PublicChat.ChannelMessageEvent -import com.vitorpamplona.quartz.nip28PublicChat.ChannelMetadataEvent -import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiPackEvent -import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiPackSelectionEvent -import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrl +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent +import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag +import com.vitorpamplona.quartz.nip30CustomEmoji.emojis +import com.vitorpamplona.quartz.nip30CustomEmoji.pack.EmojiPackEvent +import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiPackSelectionEvent import com.vitorpamplona.quartz.nip30CustomEmoji.taggedEmojis -import com.vitorpamplona.quartz.nip34Git.GitReplyEvent import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent +import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning import com.vitorpamplona.quartz.nip37Drafts.DraftEvent import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent @@ -116,29 +145,38 @@ import com.vitorpamplona.quartz.nip51Lists.BookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.GeneralListEvent import com.vitorpamplona.quartz.nip51Lists.MuteListEvent import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent -import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.nip56Reports.ReportEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetup -import com.vitorpamplona.quartz.nip59Giftwrap.GiftWrapEvent -import com.vitorpamplona.quartz.nip59Giftwrap.SealedRumorEvent +import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplits +import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiser import com.vitorpamplona.quartz.nip59Giftwrap.WrappedEvent +import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent +import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.nip65RelayList.RelayUrlFormatter import com.vitorpamplona.quartz.nip68Picture.PictureEvent import com.vitorpamplona.quartz.nip68Picture.PictureMeta +import com.vitorpamplona.quartz.nip68Picture.pictureIMeta import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent +import com.vitorpamplona.quartz.nip71Video.VideoMeta import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryRequestEvent import com.vitorpamplona.quartz.nip92IMeta.IMetaTag -import com.vitorpamplona.quartz.nip94FileMetadata.Dimension +import com.vitorpamplona.quartz.nip92IMeta.imetas import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent -import com.vitorpamplona.quartz.nip96FileStorage.FileServersEvent +import com.vitorpamplona.quartz.nip94FileMetadata.blurhash +import com.vitorpamplona.quartz.nip94FileMetadata.dimension +import com.vitorpamplona.quartz.nip94FileMetadata.fileSize +import com.vitorpamplona.quartz.nip94FileMetadata.hash +import com.vitorpamplona.quartz.nip94FileMetadata.magnet +import com.vitorpamplona.quartz.nip94FileMetadata.mimeType +import com.vitorpamplona.quartz.nip94FileMetadata.originalHash +import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag +import com.vitorpamplona.quartz.nip96FileStorage.config.FileServersEvent import com.vitorpamplona.quartz.nip98HttpAuth.HTTPAuthorizationEvent -import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent -import com.vitorpamplona.quartz.nip99Classifieds.Price import com.vitorpamplona.quartz.utils.DualCase import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi @@ -161,8 +199,8 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import org.czeal.rfc3986.URIReference import java.math.BigDecimal +import java.util.Base64 import java.util.Locale -import java.util.UUID import kotlin.coroutines.cancellation.CancellationException import kotlin.coroutines.resume @@ -955,7 +993,7 @@ class Account( onReady: (LiveFollowList) -> Unit, ) { listEvent.privateTags(signer) { privateTagList -> - val users = (listEvent.taggedUsers() + listEvent.filterUsers(privateTagList)).toSet() + val users = (listEvent.taggedUserIds() + listEvent.filterUsers(privateTagList)).toSet() onReady( LiveFollowList( authors = users, @@ -1256,7 +1294,7 @@ class Account( } } - fun sendKind3RelayList(relays: Map) { + fun sendKind3RelayList(relays: Map) { if (!isWriteable()) return val contactList = userProfile().latestContactList @@ -1360,9 +1398,9 @@ class Account( val users = noteEvent.groupMembers().toList() if (reaction.startsWith(":")) { - val emojiUrl = EmojiUrl.decode(reaction) + val emojiUrl = EmojiUrlTag.decode(reaction) if (emojiUrl != null) { - note.event?.let { + note.toEventHint()?.let { NIP17Factory().createReactionWithinGroup( emojiUrl = emojiUrl, originalNote = it, @@ -1377,7 +1415,7 @@ class Account( } } - note.event?.let { + note.toEventHint()?.let { NIP17Factory().createReactionWithinGroup( content = reaction, originalNote = it, @@ -1390,10 +1428,12 @@ class Account( return } else { if (reaction.startsWith(":")) { - val emojiUrl = EmojiUrl.decode(reaction) + val emojiUrl = EmojiUrlTag.decode(reaction) if (emojiUrl != null) { note.event?.let { - ReactionEvent.create(emojiUrl, it, signer) { + signer.sign( + ReactionEvent.build(emojiUrl, EventHintBundle(it, note.relayHintUrl())), + ) { Amethyst.instance.client.send(it) LocalCache.consume(it) } @@ -1403,8 +1443,10 @@ class Account( } } - note.event?.let { - ReactionEvent.create(reaction, it, signer) { + note.toEventHint()?.let { + signer.sign( + ReactionEvent.build(reaction, it), + ) { Amethyst.instance.client.send(it) LocalCache.consume(it) } @@ -1562,13 +1604,6 @@ class Account( return } - note.event?.let { - ReactionEvent.createWarning(it, signer) { - Amethyst.instance.client.send(it) - LocalCache.justConsume(it, null) - } - } - note.event?.let { ReportEvent.create(it, type, signer, content) { Amethyst.instance.client.send(it) @@ -1605,7 +1640,9 @@ class Account( if (myNoteVersions.isNotEmpty()) { // chunks in 200 elements to avoid going over the 65KB limit for events. myNoteVersions.chunked(200).forEach { chunkedList -> - DeletionEvent.create(chunkedList, signer) { deletionEvent -> + signer.sign( + DeletionEvent.build(chunkedList), + ) { deletionEvent -> Amethyst.instance.client.send(deletionEvent) LocalCache.justConsume(deletionEvent, null) } @@ -1620,8 +1657,10 @@ class Account( ): HTTPAuthorizationEvent? { if (!isWriteable()) return null + val template = HTTPAuthorizationEvent.build(url, method, body) + return tryAndWait { continuation -> - HTTPAuthorizationEvent.create(url, method, body, signer) { + signer.sign(template) { continuation.resume(it) } } @@ -1656,24 +1695,26 @@ class Account( suspend fun boost(note: Note) { if (!isWriteable()) return + val noteEvent = note.event ?: return if (note.hasBoostedInTheLast5Minutes(userProfile())) { // has already bosted in the past 5mins return } - note.event?.let { - if (it.kind == 1) { - RepostEvent.create(it, signer) { - Amethyst.instance.client.send(it) - LocalCache.justConsume(it, null) - } + val noteHint = note.relayHintUrl() + val authorHint = note.author?.bestRelayHint() + + val template = + if (noteEvent.kind == 1) { + RepostEvent.build(noteEvent, noteHint, authorHint) } else { - GenericRepostEvent.create(it, signer) { - Amethyst.instance.client.send(it) - LocalCache.justConsume(it, null) - } + GenericRepostEvent.build(noteEvent, noteHint, authorHint) } + + signer.sign(template) { + Amethyst.instance.client.send(it) + LocalCache.justConsume(it, null) } } @@ -1706,10 +1747,19 @@ class Account( Log.d("Pending Attestations", "Updating ${settings.pendingAttestations.value.size} pending attestations") settings.pendingAttestations.value.forEach { pair -> - val newAttestation = OtsEvent.upgrade(pair.value, pair.key) + val otsState = OtsEvent.upgrade(Base64.getDecoder().decode(pair.value), pair.key) - if (pair.value != newAttestation) { - OtsEvent.create(pair.key, newAttestation, signer) { + if (otsState != null) { + val hint = LocalCache.getNoteIfExists(pair.key)?.toEventHint() + + val template = + if (hint != null) { + OtsEvent.build(hint, otsState) + } else { + OtsEvent.build(pair.key, otsState) + } + + signer.sign(template) { LocalCache.justConsume(it, null) Amethyst.instance.client.send(it) @@ -1732,7 +1782,7 @@ class Account( val id = note.event?.id ?: note.idHex - settings.addPendingAttestation(id, OtsEvent.stamp(id)) + settings.addPendingAttestation(id, Base64.getEncoder().encodeToString(OtsEvent.stamp(id))) } fun follow(user: User) { @@ -1747,14 +1797,14 @@ class Account( } } else { ContactListEvent.createFromScratch( - followUsers = listOf(Contact(user.pubkeyHex, null)), + followUsers = listOf(ContactTag(user.pubkeyHex, user.bestRelayHint(), null)), followTags = emptyList(), followGeohashes = emptyList(), followCommunities = emptyList(), followEvents = DefaultChannels.toList(), relayUse = Constants.defaultRelays.associate { - it.url to ContactListEvent.ReadWrite(it.read, it.write) + it.url to ReadWrite(it.read, it.write) }, signer = signer, ) { @@ -1783,7 +1833,7 @@ class Account( followEvents = DefaultChannels.toList().plus(channel.idHex), relayUse = Constants.defaultRelays.associate { - it.url to ContactListEvent.ReadWrite(it.read, it.write) + it.url to ReadWrite(it.read, it.write) }, signer = signer, ) { @@ -1806,7 +1856,7 @@ class Account( } else { val relays = Constants.defaultRelays.associate { - it.url to ContactListEvent.ReadWrite(it.read, it.write) + it.url to ReadWrite(it.read, it.write) } ContactListEvent.createFromScratch( followUsers = emptyList(), @@ -1846,7 +1896,7 @@ class Account( followEvents = DefaultChannels.toList(), relayUse = Constants.defaultRelays.associate { - it.url to ContactListEvent.ReadWrite(it.read, it.write) + it.url to ReadWrite(it.read, it.write) }, signer = signer, ) { @@ -1877,7 +1927,7 @@ class Account( followEvents = DefaultChannels.toList(), relayUse = Constants.defaultRelays.associate { - it.url to ContactListEvent.ReadWrite(it.read, it.write) + it.url to ReadWrite(it.read, it.write) }, signer = signer, onReady = this::onNewEventCreated, @@ -1969,27 +2019,25 @@ class Account( byteArray: ByteArray, headerInfo: FileHeader, alt: String?, - sensitiveContent: Boolean, + contentWarningReason: String?, onReady: (Pair) -> Unit, ) { if (!isWriteable()) return - FileStorageEvent.create( - mimeType = headerInfo.mimeType ?: "", - data = byteArray, - signer = signer, - ) { data -> - FileStorageHeaderEvent.create( - data, - mimeType = headerInfo.mimeType, - hash = headerInfo.hash, - size = headerInfo.size.toString(), - dimensions = headerInfo.dim, - blurhash = headerInfo.blurHash?.blurhash, - alt = alt, - sensitiveContent = sensitiveContent, - signer = signer, - ) { signedEvent -> + signer.sign(FileStorageEvent.build(byteArray, headerInfo.mimeType)) { data -> + val template = + FileStorageHeaderEvent.build(EventHintBundle(data, userProfile().bestRelayHint()), alt) { + hash(headerInfo.hash) + fileSize(headerInfo.size) + + headerInfo.mimeType?.let { mimeType(it) } + headerInfo.dim?.let { dimension(it) } + headerInfo.blurHash?.let { blurhash(it.blurhash) } + + contentWarningReason?.let { contentWarning(contentWarningReason) } + } + + signer.sign(template) { signedEvent -> onReady( Pair(data, signedEvent), ) @@ -2048,33 +2096,34 @@ class Account( magnetUri: String?, headerInfo: FileHeader, alt: String?, - sensitiveContent: Boolean, + contentWarningReason: String? = null, originalHash: String? = null, onReady: (FileHeaderEvent) -> Unit, ) { if (!isWriteable()) return - FileHeaderEvent.create( - url = imageUrl, - magnetUri = magnetUri, - mimeType = headerInfo.mimeType, - hash = headerInfo.hash, - size = headerInfo.size.toString(), - dimensions = headerInfo.dim, - blurhash = headerInfo.blurHash?.blurhash, - alt = alt, - originalHash = originalHash, - sensitiveContent = sensitiveContent, - signer = signer, - ) { event -> - onReady(event) - } + signer.sign( + FileHeaderEvent.build(imageUrl, alt) { + hash(headerInfo.hash) + fileSize(headerInfo.size) + + headerInfo.mimeType?.let { mimeType(it) } + headerInfo.dim?.let { dimension(it) } + headerInfo.blurHash?.let { blurhash(it.blurhash) } + + originalHash?.let { originalHash(it) } + magnetUri?.let { magnet(it) } + + contentWarningReason?.let { contentWarning(contentWarningReason) } + }, + onReady, + ) } fun sendAllAsOnePictureEvent( urlHeaderInfo: Map, caption: String?, - sensitiveContent: Boolean, + contentWarningReason: String?, relayList: List, onReady: (Note) -> Unit, ) { @@ -2087,19 +2136,28 @@ class Account( it.value.dim, caption, it.value.hash, - it.value.size.toLong(), + it.value.size, + null, emptyList(), emptyList(), ) } - PictureEvent.create( - images = iMetas, - msg = caption, - markAsSensitive = sensitiveContent, - signer = signer, - ) { event -> - sendHeader(event, relayList = relayList, onReady) + signer.sign( + PictureEvent.build(iMetas, caption ?: "") { + caption?.let { + hashtags(findHashtags(it)) + references(findURLs(it)) + quotes(findNostrUris(it)) + } + // add zap splits + // add zap raiser + // add geohashes + // add title + contentWarningReason?.let { contentWarning(contentWarningReason) } + }, + ) { + sendHeader(it, relayList = relayList, onReady) } } @@ -2108,7 +2166,7 @@ class Account( magnetUri: String?, headerInfo: FileHeader, alt: String?, - sensitiveContent: Boolean, + contentWarningReason: String?, originalHash: String? = null, relayList: List, onReady: (Note) -> Unit, @@ -2118,276 +2176,164 @@ class Account( val isImage = headerInfo.mimeType?.startsWith("image/") == true || RichTextParser.isImageUrl(url) val isVideo = headerInfo.mimeType?.startsWith("video/") == true || RichTextParser.isVideoUrl(url) - if (isImage) { - PictureEvent.create( - url = url, - msg = alt, - mimeType = headerInfo.mimeType, - hash = headerInfo.hash, - size = headerInfo.size.toLong(), - dimensions = headerInfo.dim, - blurhash = headerInfo.blurHash?.blurhash, - markAsSensitive = sensitiveContent, - alt = alt, - signer = signer, - ) { event -> - sendHeader(event, relayList = relayList, onReady) - } - } else if (isVideo && headerInfo.dim != null) { - if (headerInfo.dim.height > headerInfo.dim.width) { - VideoVerticalEvent.create( - url = url, - mimeType = headerInfo.mimeType, - hash = headerInfo.hash, - size = headerInfo.size, - dimensions = headerInfo.dim, - blurhash = headerInfo.blurHash?.blurhash, - alt = alt, - sensitiveContent = sensitiveContent, - signer = signer, - ) { event -> - sendHeader(event, relayList = relayList, onReady) + val template = + if (isImage) { + PictureEvent.build(alt ?: "") { + alt?.let { + hashtags(findHashtags(it)) + references(findURLs(it)) + quotes(findNostrUris(it)) + } + pictureIMeta( + url, + headerInfo.mimeType, + headerInfo.blurHash?.blurhash, + headerInfo.dim, + headerInfo.hash, + headerInfo.size, + alt, + ) + // add zap splits + // add zap raiser + // add geohashes + // add title + contentWarningReason?.let { contentWarning(contentWarningReason) } + } + } else if (isVideo && headerInfo.dim != null) { + val videoMeta = + VideoMeta( + url = url, + hash = headerInfo.hash, + size = headerInfo.size, + mimeType = headerInfo.mimeType, + dimension = headerInfo.dim, + blurhash = headerInfo.blurHash?.blurhash, + alt = alt, + ) + + if (headerInfo.dim.height > headerInfo.dim.width) { + VideoVerticalEvent.build(videoMeta, alt ?: "") { + contentWarningReason?.let { contentWarning(contentWarningReason) } + } + } else { + VideoHorizontalEvent.build(videoMeta, alt ?: "") { + contentWarningReason?.let { contentWarning(contentWarningReason) } + } } } else { - VideoHorizontalEvent.create( - url = url, - mimeType = headerInfo.mimeType, - hash = headerInfo.hash, - size = headerInfo.size, - dimensions = headerInfo.dim, - blurhash = headerInfo.blurHash?.blurhash, - alt = alt, - sensitiveContent = sensitiveContent, - signer = signer, - ) { event -> - sendHeader(event, relayList = relayList, onReady) + FileHeaderEvent.build(url, alt) { + hash(headerInfo.hash) + fileSize(headerInfo.size) + + headerInfo.mimeType?.let { mimeType(it) } + headerInfo.dim?.let { dimension(it) } + headerInfo.blurHash?.let { blurhash(it.blurhash) } + + originalHash?.let { originalHash(it) } + magnetUri?.let { magnet(it) } + + contentWarningReason?.let { contentWarning(contentWarningReason) } + } + } + + signer.sign(template) { + sendHeader(it, relayList = relayList, onReady) + } + } + + fun signAndSend( + draftTag: String?, + template: EventTemplate, + ) { + if (draftTag != null) { + if (template.content.isEmpty()) { + deleteDraft(draftTag) + } else { + signer.assembleRumor(template) { rumor -> + DraftEvent.create(draftTag, rumor, emptyList(), signer) { draftEvent -> + sendDraftEvent(draftEvent) + } } } } else { - FileHeaderEvent.create( - url = url, - magnetUri = magnetUri, - mimeType = headerInfo.mimeType, - hash = headerInfo.hash, - size = headerInfo.size.toString(), - dimensions = headerInfo.dim, - blurhash = headerInfo.blurHash?.blurhash, - alt = alt, - originalHash = originalHash, - sensitiveContent = sensitiveContent, - signer = signer, - ) { event -> - sendHeader(event, relayList = relayList, onReady) + signer.sign(template) { + LocalCache.justConsume(it, null) + Amethyst.instance.client.send(it) } } } - fun sendClassifieds( - title: String, - price: Price, - condition: ClassifiedsEvent.CONDITION, - location: String, - category: String, - message: String, - replyTo: List?, - mentions: List?, - directMentions: Set, - zapReceiver: List? = null, - wantsToMarkAsSensitive: Boolean, - zapRaiserAmount: Long? = null, - relayList: List, - geohash: String? = null, - imetas: List? = null, - emojis: List? = null, + fun signAndSend( draftTag: String?, + template: EventTemplate, + relayList: List, + broadcastNotes: List, + ) = signAndSend(draftTag, template, relayList, mapEntitiesToNotes(broadcastNotes).toSet()) + + fun signAndSend( + draftTag: String?, + template: EventTemplate, + relayList: List, + broadcastNotes: Set, ) { - if (!isWriteable()) return - - val repliesToHex = replyTo?.filter { it.address() == null }?.map { it.idHex } - val mentionsHex = mentions?.map { it.pubkeyHex } - val addresses = replyTo?.mapNotNull { it.address() } - - ClassifiedsEvent.create( - dTag = UUID.randomUUID().toString(), - title = title, - price = price, - condition = condition, - summary = message, - image = null, - location = location, - category = category, - message = message, - replyTos = repliesToHex, - mentions = mentionsHex, - addresses = addresses, - zapReceiver = zapReceiver, - markAsSensitive = wantsToMarkAsSensitive, - zapRaiserAmount = zapRaiserAmount, - directMentions = directMentions, - geohash = geohash, - imetas = imetas, - emojis = emojis, - signer = signer, - isDraft = draftTag != null, - ) { - if (draftTag != null) { - if (message.isBlank()) { - deleteDraft(draftTag) - } else { - DraftEvent.create(draftTag, it, emptyList(), signer) { draftEvent -> - sendDraftEvent(draftEvent) - } + if (draftTag != null) { + signer.assembleRumor(template) { rumor -> + DraftEvent.create(draftTag, rumor, emptyList(), signer) { draftEvent -> + sendDraftEvent(draftEvent) } - } else { - Amethyst.instance.client.send(it, relayList = relayList) + } + } else { + signer.sign(template) { LocalCache.justConsume(it, null) + Amethyst.instance.client.send(it, relayList = relayList) - replyTo?.forEach { it.event?.let { Amethyst.instance.client.send(it, relayList = relayList) } } - addresses?.forEach { - LocalCache.getAddressableNoteIfExists(it.toTag())?.event?.let { - Amethyst.instance.client.send(it, relayList = relayList) - } - } + broadcastNotes.forEach { it.event?.let { Amethyst.instance.client.send(it, relayList = relayList) } } } } } - fun sendGitReply( - message: String, - replyTo: List?, - mentions: List?, - repository: ATag?, - zapReceiver: List? = null, - wantsToMarkAsSensitive: Boolean, - zapRaiserAmount: Long? = null, - replyingTo: String?, - root: String?, - directMentions: Set, - forkedFrom: Event?, - relayList: List, - geohash: String? = null, - imetas: List? = null, - emojis: List? = null, + fun signAndSendWithList( draftTag: String?, + template: EventTemplate, + relayList: List, + broadcastNotes: Set, ) { - if (!isWriteable()) return - - val repliesToHex = replyTo?.filter { it.address() == null }?.map { it.idHex } - val mentionsHex = mentions?.map { it.pubkeyHex } - val addresses = listOfNotNull(repository) + (replyTo?.mapNotNull { it.address() } ?: emptyList()) - - GitReplyEvent.create( - msg = message, - replyTos = repliesToHex, - mentions = mentionsHex, - addresses = addresses, - zapReceiver = zapReceiver, - markAsSensitive = wantsToMarkAsSensitive, - zapRaiserAmount = zapRaiserAmount, - replyingTo = replyingTo, - root = root, - directMentions = directMentions, - geohash = geohash, - imetas = imetas, - emojis = emojis, - forkedFrom = forkedFrom, - signer = signer, - isDraft = draftTag != null, - ) { - if (draftTag != null) { - if (message.isBlank()) { - deleteDraft(draftTag) - } else { - DraftEvent.create(draftTag, it, signer) { draftEvent -> - sendDraftEvent(draftEvent) - } + if (draftTag != null) { + signer.assembleRumor(template) { rumor -> + DraftEvent.create(draftTag, rumor, emptyList(), signer) { draftEvent -> + sendDraftEvent(draftEvent) } - } else { - Amethyst.instance.client.send(it, relayList = relayList) + } + } else { + signer.sign(template) { + val connect = + relayList.map { + val normalizedUrl = RelayUrlFormatter.normalize(it) + RelaySetupInfoToConnect( + normalizedUrl, + shouldUseTorForClean(normalizedUrl), + true, + true, + setOf(FeedType.GLOBAL), + ) + } + LocalCache.justConsume(it, null) - - // broadcast replied notes - replyingTo?.let { - LocalCache.getNoteIfExists(replyingTo)?.event?.let { - Amethyst.instance.client.send(it, relayList = relayList) - } - } - replyTo?.forEach { it.event?.let { Amethyst.instance.client.send(it, relayList = relayList) } } - addresses?.forEach { - LocalCache.getAddressableNoteIfExists(it.toTag())?.event?.let { - Amethyst.instance.client.send(it, relayList = relayList) - } - } + Amethyst.instance.client.sendPrivately(it, relayList = connect) + broadcastNotes.forEach { it.event?.let { Amethyst.instance.client.sendPrivately(it, relayList = connect) } } } } } fun sendTorrentComment( - message: String, - replyTo: List?, - mentions: List?, - zapReceiver: List? = null, - wantsToMarkAsSensitive: Boolean, - zapRaiserAmount: Long? = null, - replyingTo: String?, - root: String, - directMentions: Set, - forkedFrom: Event?, - relayList: List, - geohash: String? = null, - imetas: List? = null, - emojis: List? = null, draftTag: String?, + template: EventTemplate, + broadcastNotes: Set, + relayList: List, ) { if (!isWriteable()) return - val repliesToHex = replyTo?.filter { it.address() == null }?.map { it.idHex } - val mentionsHex = mentions?.map { it.pubkeyHex } - val addresses = replyTo?.mapNotNull { it.address() } ?: emptyList() - - TorrentCommentEvent.create( - message = message, - replyTos = repliesToHex, - mentions = mentionsHex, - zapReceiver = zapReceiver, - markAsSensitive = wantsToMarkAsSensitive, - zapRaiserAmount = zapRaiserAmount, - replyingTo = replyingTo, - torrent = root, - directMentions = directMentions, - geohash = geohash, - imetas = imetas, - emojis = emojis, - forkedFrom = forkedFrom, - signer = signer, - isDraft = draftTag != null, - ) { - if (draftTag != null) { - if (message.isBlank()) { - deleteDraft(draftTag) - } else { - DraftEvent.create(draftTag, it, signer) { draftEvent -> - sendDraftEvent(draftEvent) - } - } - } else { - Amethyst.instance.client.send(it, relayList = relayList) - LocalCache.justConsume(it, null) - - // broadcast replied notes - replyingTo?.let { - LocalCache.getNoteIfExists(replyingTo)?.event?.let { - Amethyst.instance.client.send(it, relayList = relayList) - } - } - replyTo?.forEach { it.event?.let { Amethyst.instance.client.send(it, relayList = relayList) } } - addresses?.forEach { - LocalCache.getAddressableNoteIfExists(it.toTag())?.event?.let { - Amethyst.instance.client.send(it, relayList = relayList) - } - } - } - } + signAndSend(draftTag, template, relayList, broadcastNotes) } fun deleteDraft(draftTag: String) { @@ -2415,221 +2361,6 @@ class Account( } } - suspend fun sendReplyComment( - message: String, - replyingTo: Note, - directMentionsUsers: Set = emptySet(), - directMentionsNotes: Set = emptySet(), - imetas: List? = null, - emojis: List? = null, - geohash: String? = null, - zapReceiver: List? = null, - wantsToMarkAsSensitive: Boolean = false, - zapRaiserAmount: Long? = null, - relayList: List, - draftTag: String? = null, - ) { - if (!isWriteable()) return - - val usersMentioned = - directMentionsUsers - .mapTo(HashSet(directMentionsUsers.size)) { - PTag(it.pubkeyHex, it.latestMetadataRelay) - } - - val addressesMentioned = - directMentionsNotes - .mapNotNullTo(HashSet(directMentionsNotes.size)) { note -> - if (note is AddressableNote) { - note.address - } else { - null - } - } - - val eventsMentioned = - directMentionsNotes - .mapNotNullTo(HashSet(directMentionsNotes.size)) { note -> - if (note !is AddressableNote) { - ETag(note.idHex, note.relayHintUrl(), note.author?.pubkeyHex) - } else { - null - } - } - - if (replyingTo.event is CommentEvent) { - CommentEvent.replyComment( - msg = message, - replyingTo = EventHintBundle(replyingTo.event as CommentEvent, replyingTo.relayHintUrl()), - usersMentioned = usersMentioned, - addressesMentioned = addressesMentioned, - eventsMentioned = eventsMentioned, - imetas = imetas, - emojis = emojis, - geohash = geohash, - zapReceiver = zapReceiver, - markAsSensitive = wantsToMarkAsSensitive, - zapRaiserAmount = zapRaiserAmount, - isDraft = draftTag != null, - signer = signer, - ) { - if (draftTag != null) { - if (message.isBlank()) { - deleteDraft(draftTag) - } else { - DraftEvent.create(draftTag, it, signer) { draftEvent -> - sendDraftEvent(draftEvent) - } - } - } else { - Amethyst.instance.client.send(it, relayList = relayList) - LocalCache.justConsume(it, null) - - replyingTo.event?.let { - Amethyst.instance.client.send(it, relayList = relayList) - } - } - } - } else { - CommentEvent.firstReplyToEvent( - msg = message, - replyingTo = EventHintBundle(replyingTo.event as Event, replyingTo.relayHintUrl()), - usersMentioned = usersMentioned, - addressesMentioned = addressesMentioned, - eventsMentioned = eventsMentioned, - imetas = imetas, - emojis = emojis, - geohash = geohash, - zapReceiver = zapReceiver, - markAsSensitive = wantsToMarkAsSensitive, - zapRaiserAmount = zapRaiserAmount, - isDraft = draftTag != null, - signer = signer, - ) { - if (draftTag != null) { - if (message.isBlank()) { - deleteDraft(draftTag) - } else { - DraftEvent.create(draftTag, it, signer) { draftEvent -> - sendDraftEvent(draftEvent) - } - } - } else { - Amethyst.instance.client.send(it, relayList = relayList) - LocalCache.justConsume(it, null) - - replyingTo.event?.let { - Amethyst.instance.client.send(it, relayList = relayList) - } - } - } - } - } - - suspend fun sendGeoComment( - message: String, - geohash: String, - replyingTo: Note? = null, - directMentionsUsers: Set = emptySet(), - directMentionsNotes: Set = emptySet(), - imetas: List? = null, - emojis: List? = null, - zapReceiver: List? = null, - wantsToMarkAsSensitive: Boolean = false, - zapRaiserAmount: Long? = null, - relayList: List, - draftTag: String? = null, - ) { - if (!isWriteable()) return - - val usersMentioned = - directMentionsUsers - .mapTo(HashSet(directMentionsUsers.size)) { - PTag(it.pubkeyHex, it.latestMetadataRelay) - } - - val addressesMentioned = - directMentionsNotes - .mapNotNullTo(HashSet(directMentionsNotes.size)) { note -> - if (note is AddressableNote) { - note.address - } else { - null - } - } - - val eventsMentioned = - directMentionsNotes - .mapNotNullTo(HashSet(directMentionsNotes.size)) { note -> - if (note !is AddressableNote) { - ETag(note.idHex, note.relayHintUrl(), note.author?.pubkeyHex) - } else { - null - } - } - - if (replyingTo != null) { - CommentEvent.replyComment( - msg = message, - replyingTo = EventHintBundle(replyingTo.event as CommentEvent, replyingTo.relayHintUrl()), - usersMentioned = usersMentioned, - addressesMentioned = addressesMentioned, - eventsMentioned = eventsMentioned, - imetas = imetas, - emojis = emojis, - zapReceiver = zapReceiver, - markAsSensitive = wantsToMarkAsSensitive, - zapRaiserAmount = zapRaiserAmount, - isDraft = draftTag != null, - signer = signer, - ) { - if (draftTag != null) { - if (message.isBlank()) { - deleteDraft(draftTag) - } else { - DraftEvent.create(draftTag, it, signer) { draftEvent -> - sendDraftEvent(draftEvent) - } - } - } else { - Amethyst.instance.client.send(it, relayList = relayList) - LocalCache.justConsume(it, null) - - replyingTo.event?.let { - Amethyst.instance.client.send(it, relayList = relayList) - } - } - } - } else { - CommentEvent.createGeoComment( - msg = message, - geohash = geohash, - usersMentioned = usersMentioned, - addressesMentioned = addressesMentioned, - eventsMentioned = eventsMentioned, - imetas = imetas, - zapReceiver = zapReceiver, - markAsSensitive = wantsToMarkAsSensitive, - zapRaiserAmount = zapRaiserAmount, - isDraft = draftTag != null, - signer = signer, - ) { - if (draftTag != null) { - if (message.isBlank()) { - deleteDraft(draftTag) - } else { - DraftEvent.create(draftTag, it, signer) { draftEvent -> - sendDraftEvent(draftEvent) - } - } - } else { - Amethyst.instance.client.send(it, relayList = relayList) - LocalCache.justConsume(it, null) - } - } - } - } - suspend fun createInteractiveStoryReadingState( root: InteractiveStoryBaseEvent, rootRelay: String?, @@ -2640,13 +2371,15 @@ class Account( val relayList = getPrivateOutBoxRelayList() - InteractiveStoryReadingStateEvent.create( - root = root, - rootRelay = rootRelay, - currentScene = readingScene, - currentSceneRelay = readingSceneRelay, - signer = signer, - ) { + val template = + InteractiveStoryReadingStateEvent.build( + root = root, + rootRelay = rootRelay, + currentScene = readingScene, + currentSceneRelay = readingSceneRelay, + ) + + signer.sign(template) { if (relayList.isNotEmpty()) { Amethyst.instance.client.sendPrivately(it, relayList = relayList) } else { @@ -2665,12 +2398,14 @@ class Account( val relayList = getPrivateOutBoxRelayList() - InteractiveStoryReadingStateEvent.update( - base = readingState, - currentScene = readingScene, - currentSceneRelay = readingSceneRelay, - signer = signer, - ) { + val template = + InteractiveStoryReadingStateEvent.update( + base = readingState, + currentScene = readingScene, + currentSceneRelay = readingSceneRelay, + ) + + signer.sign(template) { if (relayList.isNotEmpty()) { Amethyst.instance.client.sendPrivately(it, relayList = relayList) } else { @@ -2680,15 +2415,30 @@ class Account( } } + fun mapEntitiesToNotes(entities: List): List = + entities.mapNotNull { + when (it) { + is NPub -> null + is NProfile -> null + is com.vitorpamplona.quartz.nip19Bech32.entities.Note -> LocalCache.getOrCreateNote(it.hex) + is NEvent -> LocalCache.getOrCreateNote(it.hex) + is NEmbed -> LocalCache.getOrCreateNote(it.event.id) + is NAddress -> LocalCache.checkGetOrCreateAddressableNote(it.aTag()) + is NSec -> null + is NRelay -> null + else -> null + } + } + suspend fun sendInteractiveStoryPrologue( baseId: String, title: String, content: String, - options: List, + options: List, summary: String? = null, image: String? = null, zapReceiver: List? = null, - wantsToMarkAsSensitive: Boolean = false, + contentWarningReason: String? = null, zapRaiserAmount: Long? = null, imetas: List? = null, draftTag: String? = null, @@ -2696,42 +2446,36 @@ class Account( ) { if (!isWriteable()) return - InteractiveStoryPrologueEvent.create( - baseId = baseId, - title = title, - content = content, - options = options, - summary = summary, - image = image, - zapReceiver = zapReceiver, - markAsSensitive = wantsToMarkAsSensitive, - zapRaiserAmount = zapRaiserAmount, - imetas = imetas, - signer = signer, - isDraft = draftTag != null, - ) { - if (draftTag != null) { - if (content.isBlank()) { - deleteDraft(draftTag) - } else { - DraftEvent.create(draftTag, it, signer) { draftEvent -> - sendDraftEvent(draftEvent) - } - } - } else { - Amethyst.instance.client.send(it, relayList = relayList) - LocalCache.justConsume(it, null) + val quotes = findNostrUris(content) + + val template = + InteractiveStoryPrologueEvent.build( + baseId = baseId, + title = title, + content = content, + options = options, + ) { + summary?.let { summary(it) } + image?.let { image(it) } + hashtags(findHashtags(content)) + references(findURLs(content)) + quotes(quotes) + zapRaiserAmount?.let { zapraiser(it) } + zapReceiver?.let { zapSplits(it) } + imetas?.let { imetas(it) } + contentWarningReason?.let { contentWarning(contentWarningReason) } } - } + + signAndSend(draftTag, template, relayList, quotes) } suspend fun sendInteractiveStoryScene( baseId: String, title: String, content: String, - options: List, + options: List, zapReceiver: List? = null, - wantsToMarkAsSensitive: Boolean = false, + contentWarningReason: String? = null, zapRaiserAmount: Long? = null, imetas: List? = null, draftTag: String? = null, @@ -2739,102 +2483,46 @@ class Account( ) { if (!isWriteable()) return - InteractiveStorySceneEvent.create( - baseId = baseId, - title = title, - content = content, - options = options, - zapReceiver = zapReceiver, - markAsSensitive = wantsToMarkAsSensitive, - zapRaiserAmount = zapRaiserAmount, - imetas = imetas, - signer = signer, - isDraft = draftTag != null, - ) { - if (draftTag != null) { - if (content.isBlank()) { - deleteDraft(draftTag) - } else { - DraftEvent.create(draftTag, it, signer) { draftEvent -> - sendDraftEvent(draftEvent) - } - } - } else { - Amethyst.instance.client.send(it, relayList = relayList) - LocalCache.justConsume(it, null) + val quotes = findNostrUris(content) + + val template = + InteractiveStorySceneEvent.build( + baseId = baseId, + title = title, + content = content, + options = options, + ) { + hashtags(findHashtags(content)) + references(findURLs(content)) + quotes(quotes) + zapRaiserAmount?.let { zapraiser(it) } + zapReceiver?.let { zapSplits(it) } + imetas?.let { imetas(it) } + contentWarningReason?.let { contentWarning(contentWarningReason) } } - } + + signAndSend(draftTag, template, relayList, mapEntitiesToNotes(quotes).toSet()) } - suspend fun sendPost( - message: String, - replyTo: List?, - mentions: List?, - tags: List? = null, - zapReceiver: List? = null, - wantsToMarkAsSensitive: Boolean, - zapRaiserAmount: Long? = null, - replyingTo: String?, - root: String?, - directMentions: Set, - forkedFrom: Event?, - relayList: List, - geohash: String? = null, - imetas: List? = null, - emojis: List? = null, + suspend fun sendAddBounty( + value: BigDecimal, + bounty: Note, draftTag: String?, + relayList: List, ) { if (!isWriteable()) return - val repliesToHex = replyTo?.filter { it.address() == null }?.map { it.idHex } - val mentionsHex = mentions?.map { it.pubkeyHex } - val addresses = replyTo?.mapNotNull { it.address() } + val event = bounty.event as? TextNoteEvent ?: return + val eventAuthor = bounty.author ?: return - TextNoteEvent.create( - msg = message, - replyTos = repliesToHex, - mentions = mentionsHex, - addresses = addresses, - extraTags = tags, - zapReceiver = zapReceiver, - markAsSensitive = wantsToMarkAsSensitive, - zapRaiserAmount = zapRaiserAmount, - replyingTo = replyingTo, - root = root, - directMentions = directMentions, - geohash = geohash, - imetas = imetas, - emojis = emojis, - forkedFrom = forkedFrom, - signer = signer, - isDraft = draftTag != null, - ) { - if (draftTag != null) { - if (message.isBlank()) { - deleteDraft(draftTag) - } else { - DraftEvent.create(draftTag, it, signer) { draftEvent -> - sendDraftEvent(draftEvent) - } - } - } else { - Amethyst.instance.client.send(it, relayList = relayList) - LocalCache.justConsume(it, null) + val template = + BountyAddValueEvent.build( + value, + EventHintBundle(event, bounty.relayHintUrl()), + eventAuthor.toPTag(), + ) - // broadcast replied notes - replyingTo?.let { - LocalCache.getNoteIfExists(replyingTo)?.event?.let { - Amethyst.instance.client.send(it, relayList = relayList) - } - } - replyTo?.forEach { it.event?.let { Amethyst.instance.client.send(it, relayList = relayList) } } - addresses?.forEach { - LocalCache.getAddressableNoteIfExists(it.toTag())?.event?.let { - Amethyst.instance.client.send(it, relayList = relayList) - } - } - } - } + signAndSend(draftTag, template, relayList, setOf(bounty)) } fun sendEdit( @@ -2860,326 +2548,94 @@ class Account( } } - fun sendPoll( - message: String, - replyTo: List?, - mentions: List?, - pollOptions: Map, - valueMaximum: Int?, - valueMinimum: Int?, - consensusThreshold: Int?, - closedAt: Int?, - zapReceiver: List? = null, - wantsToMarkAsSensitive: Boolean, - zapRaiserAmount: Long? = null, - relayList: List, - geohash: String? = null, - imetas: List? = null, - emojis: List? = null, - draftTag: String?, - ) { - if (!isWriteable()) return - - val repliesToHex = replyTo?.map { it.idHex } - val mentionsHex = mentions?.map { it.pubkeyHex } - val addresses = replyTo?.mapNotNull { it.address() } - - PollNoteEvent.create( - msg = message, - replyTos = repliesToHex, - mentions = mentionsHex, - addresses = addresses, - signer = signer, - pollOptions = pollOptions, - valueMaximum = valueMaximum, - valueMinimum = valueMinimum, - consensusThreshold = consensusThreshold, - closedAt = closedAt, - zapReceiver = zapReceiver, - markAsSensitive = wantsToMarkAsSensitive, - zapRaiserAmount = zapRaiserAmount, - geohash = geohash, - imetas = imetas, - isDraft = draftTag != null, - ) { - if (draftTag != null) { - if (message.isBlank()) { - deleteDraft(draftTag) - } else { - DraftEvent.create(draftTag, it, signer) { draftEvent -> - sendDraftEvent(draftEvent) - } - } - } else { - Amethyst.instance.client.send(it, relayList = relayList) - LocalCache.justConsume(it, null) - - // Rebroadcast replies and tags to the current relay set - replyTo?.forEach { it.event?.let { Amethyst.instance.client.send(it, relayList = relayList) } } - addresses?.forEach { - LocalCache.getAddressableNoteIfExists(it.toTag())?.event?.let { - Amethyst.instance.client.send(it, relayList = relayList) - } - } - } - } - } - - fun sendChannelMessage( - message: String, - toChannel: String, - replyTo: List?, - mentions: List?, - zapReceiver: List? = null, - wantsToMarkAsSensitive: Boolean, - zapRaiserAmount: Long? = null, - directMentions: Set, - geohash: String? = null, - imetas: List? = null, - emojis: List? = null, - draftTag: String?, - ) { - if (!isWriteable()) return - - val repliesToHex = replyTo?.map { it.idHex } - val mentionsHex = mentions?.map { it.pubkeyHex } - - ChannelMessageEvent.create( - message = message, - channel = toChannel, - replyTos = repliesToHex, - mentions = mentionsHex, - zapReceiver = zapReceiver, - markAsSensitive = wantsToMarkAsSensitive, - zapRaiserAmount = zapRaiserAmount, - directMentions = directMentions, - geohash = geohash, - imetas = imetas, - emojis = emojis, - signer = signer, - isDraft = draftTag != null, - ) { - if (draftTag != null) { - if (message.isBlank()) { - deleteDraft(draftTag) - } else { - DraftEvent.create(draftTag, it, signer) { draftEvent -> - sendDraftEvent(draftEvent) - } - } - } else { - Amethyst.instance.client.send(it) - LocalCache.justConsume(it, null) - } - } - } - - fun sendLiveMessage( - message: String, - toChannel: ATag, - replyTo: List?, - mentions: List?, - zapReceiver: List? = null, - wantsToMarkAsSensitive: Boolean, - zapRaiserAmount: Long? = null, - geohash: String? = null, - imetas: List? = null, - emojis: List? = null, - draftTag: String?, - ) { - if (!isWriteable()) return - - // val repliesToHex = listOfNotNull(replyingTo?.idHex).ifEmpty { null } - val repliesToHex = replyTo?.map { it.idHex } - val mentionsHex = mentions?.map { it.pubkeyHex } - - LiveActivitiesChatMessageEvent.create( - message = message, - activity = toChannel, - replyTos = repliesToHex, - mentions = mentionsHex, - zapReceiver = zapReceiver, - markAsSensitive = wantsToMarkAsSensitive, - zapRaiserAmount = zapRaiserAmount, - geohash = geohash, - imetas = imetas, - emojis = emojis, - signer = signer, - isDraft = draftTag != null, - ) { - if (draftTag != null) { - if (message.isBlank()) { - deleteDraft(draftTag) - } else { - DraftEvent.create(draftTag, it, signer) { draftEvent -> - sendDraftEvent(draftEvent) - } - } - } else { - Amethyst.instance.client.send(it) - LocalCache.justConsume(it, null) - } - } - } - fun sendPrivateMessage( message: String, toUser: User, replyingTo: Note? = null, mentions: List?, zapReceiver: List? = null, - wantsToMarkAsSensitive: Boolean, + contentWarningReason: String? = null, zapRaiserAmount: Long? = null, geohash: String? = null, imetas: List? = null, + emojis: List? = null, draftTag: String?, ) { sendPrivateMessage( message, - toUser.pubkeyHex, + toUser.toPTag(), replyingTo, mentions, zapReceiver, - wantsToMarkAsSensitive, + contentWarningReason, zapRaiserAmount, geohash, imetas, + emojis, draftTag, ) } fun sendPrivateMessage( message: String, - toUser: HexKey, + toUser: PTag, replyingTo: Note? = null, mentions: List?, zapReceiver: List? = null, - wantsToMarkAsSensitive: Boolean, + contentWarningReason: String? = null, zapRaiserAmount: Long? = null, geohash: String? = null, imetas: List? = null, + emojis: List? = null, draftTag: String?, ) { if (!isWriteable()) return - val repliesToHex = listOfNotNull(replyingTo?.idHex).ifEmpty { null } - val mentionsHex = mentions?.map { it.pubkeyHex } + signer.nip04Encrypt( + PrivateDmEvent.prepareMessageToEncrypt(message, imetas), + toUser.pubKey, + ) { encryptedContent -> + val template = + PrivateDmEvent.build(toUser, encryptedContent) { + replyingTo?.let { reply(it.toEId()) } - PrivateDmEvent.create( - recipientPubKey = toUser, - publishedRecipientPubKey = toUser, - msg = message, - replyTos = repliesToHex, - mentions = mentionsHex, - zapReceiver = zapReceiver, - markAsSensitive = wantsToMarkAsSensitive, - zapRaiserAmount = zapRaiserAmount, - geohash = geohash, - imetas = imetas, - signer = signer, - advertiseNip18 = false, - isDraft = draftTag != null, - ) { - if (draftTag != null) { - if (message.isBlank()) { - deleteDraft(draftTag) - } else { - DraftEvent.create(draftTag, it, emptyList(), signer) { draftEvent -> - sendDraftEvent(draftEvent) - } + geohash?.let { geohash(it) } + zapRaiserAmount?.let { zapraiser(it) } + zapReceiver?.let { zapSplits(it) } + emojis?.let { emojis(it) } + contentWarningReason?.let { contentWarning(contentWarningReason) } } - } else { - Amethyst.instance.client.send(it) - LocalCache.consume(it, null) - } + + signAndSend(draftTag, template) } } - fun sendNIP17EncryptedFile( - url: String, - toUsers: List, - replyingTo: Note? = null, - contentType: String?, - algo: String, - key: ByteArray, - nonce: ByteArray? = null, - originalHash: String? = null, - hash: String? = null, - size: Int? = null, - dimensions: Dimension? = null, - blurhash: String? = null, - sensitiveContent: Boolean? = null, - alt: String?, - ) { + fun sendNIP17EncryptedFile(template: EventTemplate) { if (!isWriteable()) return - val repliesToHex = listOfNotNull(replyingTo?.idHex).ifEmpty { null } - - NIP17Factory().createEncryptedFileNIP17( - url = url, - to = toUsers, - repliesToHex = repliesToHex, - contentType = contentType, - algo = algo, - key = key, - nonce = nonce, - originalHash = originalHash, - hash = hash, - size = size, - dimensions = dimensions, - blurhash = blurhash, - sensitiveContent = sensitiveContent, - alt = alt, - draftTag = null, - signer = signer, - ) { + NIP17Factory().createEncryptedFileNIP17(template, signer) { broadcastPrivately(it) } } fun sendNIP17PrivateMessage( - message: String, - toUsers: List, - subject: String? = null, - replyingTo: Note? = null, - mentions: List?, - zapReceiver: List? = null, - wantsToMarkAsSensitive: Boolean, - zapRaiserAmount: Long? = null, - geohash: String? = null, - imetas: List? = null, - emojis: List? = null, + template: EventTemplate, draftTag: String? = null, ) { if (!isWriteable()) return - val repliesToHex = listOfNotNull(replyingTo?.idHex).ifEmpty { null } - val mentionsHex = mentions?.map { it.pubkeyHex } - - NIP17Factory().createMsgNIP17( - msg = message, - to = toUsers, - subject = subject, - replyTos = repliesToHex, - mentions = mentionsHex, - zapReceiver = zapReceiver, - markAsSensitive = wantsToMarkAsSensitive, - zapRaiserAmount = zapRaiserAmount, - geohash = geohash, - imetas = imetas, - emojis = emojis, - draftTag = draftTag, - signer = signer, - ) { - if (draftTag != null) { - if (message.isBlank()) { - deleteDraft(draftTag) - } else { - DraftEvent.create(draftTag, it.msg, emptyList(), signer) { draftEvent -> + if (draftTag != null) { + if (template.content.isEmpty()) { + deleteDraft(draftTag) + } else { + signer.assembleRumor(template) { + DraftEvent.create(draftTag, it, emptyList(), signer) { draftEvent -> sendDraftEvent(draftEvent) } } - } else { + } + } else { + NIP17Factory().createMessageNIP17(template, signer) { broadcastPrivately(it) } } @@ -3261,19 +2717,21 @@ class Account( } } - fun sendCreateNewChannel( - name: String, - about: String, - picture: String, - ) { + fun sendChangeChannel(template: EventTemplate) { if (!isWriteable()) return - ChannelCreateEvent.create( - name = name, - about = about, - picture = picture, - signer = signer, - ) { + signer.sign(template) { + Amethyst.instance.client.send(it) + LocalCache.justConsume(it, null) + + it.channelId()?.let { LocalCache.getChannelIfExists(it)?.let { follow(it) } } + } + } + + fun sendCreateNewChannel(template: EventTemplate) { + if (!isWriteable()) return + + signer.sign(template) { Amethyst.instance.client.send(it) LocalCache.justConsume(it, null) @@ -3311,7 +2769,9 @@ class Account( Amethyst.instance.client.send(event) LocalCache.justConsume(event, null) - DeletionEvent.createForVersionOnly(listOf(event), signer) { event2 -> + signer.sign( + DeletionEvent.buildForVersionOnly(listOf(event)), + ) { event2 -> Amethyst.instance.client.send(event2) LocalCache.justConsume(event2, null) } @@ -3320,19 +2780,16 @@ class Account( fun removeEmojiPack( usersEmojiList: Note, - emojiList: Note, + emojiPack: Note, ) { if (!isWriteable()) return val noteEvent = usersEmojiList.event if (noteEvent !is EmojiPackSelectionEvent) return - val emojiListEvent = emojiList.event - if (emojiListEvent !is EmojiPackEvent) return + val emojiPackEvent = emojiPack.event + if (emojiPackEvent !is EmojiPackEvent) return - EmojiPackSelectionEvent.create( - noteEvent.taggedAddresses().filter { it != emojiListEvent.address() }, - signer, - ) { + signer.sign(EmojiPackSelectionEvent.remove(noteEvent, emojiPackEvent)) { Amethyst.instance.client.send(it) LocalCache.justConsume(it, null) } @@ -3340,17 +2797,16 @@ class Account( fun addEmojiPack( usersEmojiList: Note, - emojiList: Note, + emojiPack: Note, ) { if (!isWriteable()) return - val emojiListEvent = emojiList.event - if (emojiListEvent !is EmojiPackEvent) return + val emojiPackEvent = emojiPack.event + if (emojiPackEvent !is EmojiPackEvent) return + + val eventHint = emojiPack.toEventHint() ?: return if (usersEmojiList.event == null) { - EmojiPackSelectionEvent.create( - listOf(emojiListEvent.address()), - signer, - ) { + signer.sign(EmojiPackSelectionEvent.build(listOf(eventHint))) { Amethyst.instance.client.send(it) LocalCache.justConsume(it, null) } @@ -3358,14 +2814,7 @@ class Account( val noteEvent = usersEmojiList.event if (noteEvent !is EmojiPackSelectionEvent) return - if (noteEvent.taggedAddresses().any { it == emojiListEvent.address() }) { - return - } - - EmojiPackSelectionEvent.create( - noteEvent.taggedAddresses().plus(emojiListEvent.address()), - signer, - ) { + signer.sign(EmojiPackSelectionEvent.add(noteEvent, eventHint)) { Amethyst.instance.client.send(it) LocalCache.justConsume(it, null) } @@ -3373,32 +2822,27 @@ class Account( } fun addToGallery( - idHex: String, + idHex: HexKey, url: String, relay: String?, blurhash: String?, - dim: Dimension?, + dim: DimensionTag?, hash: String?, mimeType: String?, ) { if (!isWriteable()) return - ProfileGalleryEntryEvent.create( - url = url, - eventid = idHex, - relayhint = relay, - blurhash = blurhash, - hash = hash, - dimensions = dim, - mimeType = mimeType, - /*magnetUri = magnetUri, - size = headerInfo.size.toString(), - dimensions = headerInfo.dim, - alt = alt, - originalHash = originalHash, */ - signer = signer, - ) { event -> - Amethyst.instance.client.send(event) - LocalCache.consume(event, null) + + signer.sign( + ProfileGalleryEntryEvent.build(url) { + fromEvent(idHex, relay) + hash?.let { hash(hash) } + mimeType?.let { mimeType(it) } + dim?.let { dimension(it) } + blurhash?.let { blurhash(it) } + }, + ) { + Amethyst.instance.client.send(it) + LocalCache.consume(it, null) } } @@ -3597,7 +3041,6 @@ class Account( PeopleListEvent.removeWord( earlierVersion = blockList, word = word, - isPrivate = true, signer = signer, ) { Amethyst.instance.client.send(it) @@ -3611,7 +3054,6 @@ class Account( MuteListEvent.removeWord( earlierVersion = muteList, word = word, - isPrivate = true, signer = signer, ) { Amethyst.instance.client.send(it) @@ -3652,7 +3094,6 @@ class Account( PeopleListEvent.removeUser( earlierVersion = blockList, pubKeyHex = pubkeyHex, - isPrivate = true, signer = signer, ) { Amethyst.instance.client.send(it) @@ -3666,7 +3107,6 @@ class Account( MuteListEvent.removeUser( earlierVersion = muteList, pubKeyHex = pubkeyHex, - isPrivate = true, signer = signer, ) { Amethyst.instance.client.send(it) @@ -3684,28 +3124,6 @@ class Account( return contactList?.taggedEventIds()?.toSet() ?: DefaultChannels } - fun sendChangeChannel( - name: String, - about: String, - picture: String, - channel: Channel, - ) { - if (!isWriteable()) return - - ChannelMetadataEvent.create( - name, - about, - picture, - originalChannelIdHex = channel.idHex, - signer = signer, - ) { - Amethyst.instance.client.send(it) - LocalCache.justConsume(it, null) - - follow(channel) - } - } - fun requestDVMContentDiscovery( dvmPublicKey: String, onReady: (event: NIP90ContentDiscoveryRequestEvent) -> Unit, @@ -3923,7 +3341,7 @@ class Account( fun saveKind3RelayList(value: List) { settings.updateLocalRelays(value.toSet()) sendKind3RelayList( - value.associate { it.url to ContactListEvent.ReadWrite(it.read, it.write) }, + value.associate { it.url to ReadWrite(it.read, it.write) }, ) } @@ -4098,23 +3516,16 @@ class Account( val serverList = getFileServersList() - if (serverList != null && serverList.tags.isNotEmpty()) { - FileServersEvent.updateRelayList( - earlierVersion = serverList, - relays = servers, - signer = signer, - ) { - Amethyst.instance.client.send(it) - LocalCache.justConsume(it, null) - } - } else { - FileServersEvent.createFromScratch( - relays = servers, - signer = signer, - ) { - Amethyst.instance.client.send(it) - LocalCache.justConsume(it, null) + val template = + if (serverList != null && serverList.tags.isNotEmpty()) { + FileServersEvent.replaceServers(serverList, servers) + } else { + FileServersEvent.build(servers) } + + signer.sign(template) { + Amethyst.instance.client.send(it) + LocalCache.justConsume(it, null) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt index c20c9c0ab..a3fb08daa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt @@ -31,12 +31,12 @@ import com.vitorpamplona.ammolite.relays.Constants import com.vitorpamplona.ammolite.relays.RelaySetupInfo import com.vitorpamplona.quartz.experimental.edits.PrivateOutboxRelayListEvent import com.vitorpamplona.quartz.nip01Core.HexKey -import com.vitorpamplona.quartz.nip01Core.KeyPair -import com.vitorpamplona.quartz.nip01Core.MetadataEvent +import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair +import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal import com.vitorpamplona.quartz.nip01Core.toHexKey import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent -import com.vitorpamplona.quartz.nip17Dm.ChatMessageRelayListEvent +import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent import com.vitorpamplona.quartz.nip51Lists.MuteListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt index 3ad4b30d1..09f4b9527 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt @@ -28,12 +28,15 @@ import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder import com.vitorpamplona.amethyst.ui.note.toShortenHex import com.vitorpamplona.ammolite.relays.BundledUpdate +import com.vitorpamplona.ammolite.relays.Relay +import com.vitorpamplona.ammolite.relays.RelayBriefInfoCache import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip19Bech32.toNAddr import com.vitorpamplona.quartz.nip19Bech32.toNEvent -import com.vitorpamplona.quartz.nip28PublicChat.ChannelCreateEvent -import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesEvent +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent +import com.vitorpamplona.quartz.nip28PublicChat.base.ChannelData +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.utils.Hex import kotlinx.coroutines.Dispatchers @@ -41,11 +44,23 @@ import kotlinx.coroutines.Dispatchers class PublicChatChannel( idHex: String, ) : Channel(idHex) { - var info = ChannelCreateEvent.ChannelData(null, null, null) + var event: ChannelCreateEvent? = null + var info = ChannelData(null, null, null, null) + + override fun relays() = info.relays ?: super.relays() fun updateChannelInfo( creator: User, - channelInfo: ChannelCreateEvent.ChannelData, + channelInfo: ChannelCreateEvent, + updatedAt: Long, + ) { + this.event = channelInfo + updateChannelInfo(creator, channelInfo.channelInfo(), updatedAt) + } + + fun updateChannelInfo( + creator: User, + channelInfo: ChannelData, updatedAt: Long, ) { this.info = channelInfo @@ -76,6 +91,8 @@ class LiveActivitiesChannel( fun address() = address + override fun relays() = info?.allRelayUrls() ?: super.relays() + fun updateChannelInfo( creator: User, channelInfo: LiveActivitiesEvent, @@ -97,6 +114,10 @@ class LiveActivitiesChannel( .isNotEmpty() } +data class Counter( + var number: Int = 0, +) + @Stable abstract class Channel( val idHex: String, @@ -105,6 +126,7 @@ abstract class Channel( var updatedMetadataAt: Long = 0 val notes = LargeCache() var lastNoteCreatedAt: Long = 0 + private var relays = mapOf() open fun id() = Hex.decode(idHex) @@ -120,6 +142,14 @@ abstract class Channel( open fun profilePicture(): String? = creator?.info?.banner + open fun relays() = + relays.keys + .toSortedSet { o1, o2 -> + val o1Count = relays[o1]?.number ?: 0 + val o2Count = relays[o2]?.number ?: 0 + o2Count.compareTo(o1Count) // descending + }.map { it.url } + open fun updateChannelInfo( creator: User, updatedAt: Long, @@ -130,12 +160,31 @@ abstract class Channel( live.invalidateData() } - fun addNote(note: Note) { + @Synchronized + fun addRelaySync(briefInfo: RelayBriefInfoCache.RelayBriefInfo) { + if (briefInfo !in relays) { + relays = relays + Pair(briefInfo, Counter(1)) + } + } + + fun addNote( + note: Note, + relay: Relay? = null, + ) { notes.put(note.idHex, note) if ((note.createdAt() ?: 0) > lastNoteCreatedAt) { lastNoteCreatedAt = note.createdAt() ?: 0 } + + if (relay != null) { + val counter = relays[relay.brief] + if (counter != null) { + counter.number++ + } else { + addRelaySync(relay.brief) + } + } } fun removeNote(note: Note) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index b1e88fb1a..0ca995c3a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -31,49 +31,51 @@ import com.vitorpamplona.amethyst.model.observables.LatestByKindWithETag import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.ammolite.relays.BundledInsert import com.vitorpamplona.ammolite.relays.Relay +import com.vitorpamplona.ammolite.relays.RelayBriefInfoCache import com.vitorpamplona.quartz.blossom.BlossomServersEvent -import com.vitorpamplona.quartz.experimental.audio.AudioHeaderEvent -import com.vitorpamplona.quartz.experimental.audio.AudioTrackEvent +import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent +import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent import com.vitorpamplona.quartz.experimental.edits.PrivateOutboxRelayListEvent import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryReadingStateEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStorySceneEvent import com.vitorpamplona.quartz.experimental.medical.FhirResourceEvent -import com.vitorpamplona.quartz.experimental.nip95.FileStorageEvent -import com.vitorpamplona.quartz.experimental.nip95.FileStorageHeaderEvent +import com.vitorpamplona.quartz.experimental.nip95.data.FileStorageEvent +import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent import com.vitorpamplona.quartz.experimental.nns.NNSEvent import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent import com.vitorpamplona.quartz.experimental.relationshipStatus.RelationshipStatusEvent import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent import com.vitorpamplona.quartz.nip01Core.HexKey -import com.vitorpamplona.quartz.nip01Core.MetadataEvent import com.vitorpamplona.quartz.nip01Core.checkSignature import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.tagValueContains -import com.vitorpamplona.quartz.nip01Core.hasValidSignature +import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.addressables.mapTaggedAddress import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedAddresses import com.vitorpamplona.quartz.nip01Core.tags.events.ETag +import com.vitorpamplona.quartz.nip01Core.tags.events.GenericETag import com.vitorpamplona.quartz.nip01Core.tags.events.forEachTaggedEventId import com.vitorpamplona.quartz.nip01Core.tags.events.isTaggedEvent import com.vitorpamplona.quartz.nip01Core.tags.events.mapTaggedEventId import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEvents import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUsers +import com.vitorpamplona.quartz.nip01Core.verify import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent import com.vitorpamplona.quartz.nip03Timestamp.VerificationState import com.vitorpamplona.quartz.nip04Dm.PrivateDmEvent import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent -import com.vitorpamplona.quartz.nip10Notes.BaseTextNoteEvent +import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent -import com.vitorpamplona.quartz.nip17Dm.ChatMessageEncryptedFileHeaderEvent -import com.vitorpamplona.quartz.nip17Dm.ChatMessageEvent -import com.vitorpamplona.quartz.nip17Dm.ChatMessageRelayListEvent -import com.vitorpamplona.quartz.nip17Dm.ChatroomKey +import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey +import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent +import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent +import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent import com.vitorpamplona.quartz.nip19Bech32.decodeEventIdAsHexOrNull @@ -82,18 +84,18 @@ import com.vitorpamplona.quartz.nip19Bech32.isATag import com.vitorpamplona.quartz.nip22Comments.CommentEvent import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent -import com.vitorpamplona.quartz.nip28PublicChat.ChannelCreateEvent -import com.vitorpamplona.quartz.nip28PublicChat.ChannelHideMessageEvent import com.vitorpamplona.quartz.nip28PublicChat.ChannelListEvent -import com.vitorpamplona.quartz.nip28PublicChat.ChannelMessageEvent -import com.vitorpamplona.quartz.nip28PublicChat.ChannelMetadataEvent -import com.vitorpamplona.quartz.nip28PublicChat.ChannelMuteUserEvent -import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiPackEvent -import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiPackSelectionEvent -import com.vitorpamplona.quartz.nip34Git.GitIssueEvent -import com.vitorpamplona.quartz.nip34Git.GitPatchEvent -import com.vitorpamplona.quartz.nip34Git.GitReplyEvent -import com.vitorpamplona.quartz.nip34Git.GitRepositoryEvent +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelHideMessageEvent +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMuteUserEvent +import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent +import com.vitorpamplona.quartz.nip30CustomEmoji.pack.EmojiPackEvent +import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiPackSelectionEvent +import com.vitorpamplona.quartz.nip34Git.issue.GitIssueEvent +import com.vitorpamplona.quartz.nip34Git.patch.GitPatchEvent +import com.vitorpamplona.quartz.nip34Git.reply.GitReplyEvent +import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent import com.vitorpamplona.quartz.nip37Drafts.DraftEvent @@ -113,8 +115,8 @@ import com.vitorpamplona.quartz.nip52Calendar.CalendarDateSlotEvent import com.vitorpamplona.quartz.nip52Calendar.CalendarEvent import com.vitorpamplona.quartz.nip52Calendar.CalendarRSVPEvent import com.vitorpamplona.quartz.nip52Calendar.CalendarTimeSlotEvent -import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesChatMessageEvent -import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesEvent +import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent import com.vitorpamplona.quartz.nip56Reports.ReportEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent @@ -122,27 +124,28 @@ import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent import com.vitorpamplona.quartz.nip58Badges.BadgeAwardEvent import com.vitorpamplona.quartz.nip58Badges.BadgeDefinitionEvent import com.vitorpamplona.quartz.nip58Badges.BadgeProfilesEvent -import com.vitorpamplona.quartz.nip59Giftwrap.GiftWrapEvent -import com.vitorpamplona.quartz.nip59Giftwrap.SealedRumorEvent import com.vitorpamplona.quartz.nip59Giftwrap.WrappedEvent +import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent +import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent +import com.vitorpamplona.quartz.nip65RelayList.RelayUrlFormatter import com.vitorpamplona.quartz.nip68Picture.PictureEvent import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent -import com.vitorpamplona.quartz.nip72ModCommunities.CommunityDefinitionEvent import com.vitorpamplona.quartz.nip72ModCommunities.CommunityListEvent -import com.vitorpamplona.quartz.nip72ModCommunities.CommunityPostApprovalEvent +import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent +import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent -import com.vitorpamplona.quartz.nip89AppHandlers.AppDefinitionEvent -import com.vitorpamplona.quartz.nip89AppHandlers.AppRecommendationEvent +import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent +import com.vitorpamplona.quartz.nip89AppHandlers.recommendation.AppRecommendationEvent import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryRequestEvent import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryResponseEvent import com.vitorpamplona.quartz.nip90Dvms.NIP90StatusEvent import com.vitorpamplona.quartz.nip90Dvms.NIP90UserDiscoveryRequestEvent import com.vitorpamplona.quartz.nip90Dvms.NIP90UserDiscoveryResponseEvent import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent -import com.vitorpamplona.quartz.nip96FileStorage.FileServersEvent +import com.vitorpamplona.quartz.nip96FileStorage.config.FileServersEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent import com.vitorpamplona.quartz.utils.Hex import com.vitorpamplona.quartz.utils.TimeUtils @@ -287,7 +290,7 @@ object LocalCache { fun getOrCreateNote(event: Event): Note = if (event is AddressableEvent) { - getOrCreateAddressableNote(event.address()) + getOrCreateAddressableNote(event.aTag()) } else { getOrCreateNote(event.id) } @@ -301,6 +304,8 @@ object LocalCache { return null } + fun checkGetOrCreateNote(atag: ATag) = getOrCreateAddressableNote(atag) + fun checkGetOrCreateNote(key: String): Note? { checkNotInMainThread() @@ -312,7 +317,7 @@ object LocalCache { val noteEvent = note.event return if (noteEvent is AddressableEvent) { // upgrade to the latest - val newNote = checkGetOrCreateAddressableNote(noteEvent.address().toTag()) + val newNote = checkGetOrCreateAddressableNote(noteEvent.aTag().toTag()) if (newNote != null && newNote.event == null) { val author = note.author ?: getOrCreateUser(noteEvent.pubKey) @@ -412,13 +417,18 @@ object LocalCache { return note } - fun getOrCreateNote(key: ETag): Note { + fun getOrCreateNote(key: GenericETag): Note { val note = getOrCreateNote(key.eventId) // Loads the user outside a Syncronized block to avoid blocking - val possibleAuthor = key.authorPubKeyHex + val possibleAuthor = key.author if (note.author == null && possibleAuthor != null) { note.author = checkGetOrCreateUser(possibleAuthor) } + val relayHint = key.relay + if (!relayHint.isNullOrBlank()) { + val relay = RelayBriefInfoCache.get(RelayUrlFormatter.normalize(relayHint)) + note.addRelayBrief(relay) + } return note } @@ -521,7 +531,7 @@ object LocalCache { val replyTo = computeReplyTo(event) - if (event is BaseTextNoteEvent && antiSpam.isSpam(event, relay)) { + if (event is BaseThreadedEvent && antiSpam.isSpam(event, relay)) { return } @@ -588,7 +598,7 @@ object LocalCache { relay: Relay?, ) { val version = getOrCreateNote(event.id) - val note = getOrCreateAddressableNote(event.address()) + val note = getOrCreateAddressableNote(event.aTag()) val author = getOrCreateUser(event.pubKey) if (version.event == null) { @@ -622,7 +632,7 @@ object LocalCache { relay: Relay?, ) { val version = getOrCreateNote(event.id) - val note = getOrCreateAddressableNote(event.address()) + val note = getOrCreateAddressableNote(event.aTag()) val author = getOrCreateUser(event.pubKey) if (version.event == null) { @@ -681,7 +691,9 @@ object LocalCache { is GenericRepostEvent -> event.boostedPost().mapNotNull { checkGetOrCreateNote(it) } + event.taggedAddresses().map { getOrCreateAddressableNote(it) } - is CommunityPostApprovalEvent -> event.approvedEvents().mapNotNull { checkGetOrCreateNote(it) } + is CommunityPostApprovalEvent -> + event.approvedEvents().mapNotNull { checkGetOrCreateNote(it) } + + event.approvedAddresses().map { checkGetOrCreateNote(it) } is ReactionEvent -> event.originalPost().mapNotNull { checkGetOrCreateNote(it) } + event.taggedAddresses().map { getOrCreateAddressableNote(it) } @@ -691,7 +703,7 @@ object LocalCache { is ChannelMessageEvent -> event .tagsWithoutCitations() - .filter { it != event.channel() } + .filter { it != event.channelId() } .mapNotNull { checkGetOrCreateNote(it) } is LiveActivitiesChatMessageEvent -> event @@ -718,7 +730,7 @@ object LocalCache { relay: Relay?, ) { val version = getOrCreateNote(event.id) - val note = getOrCreateAddressableNote(event.address()) + val note = getOrCreateAddressableNote(event.aTag()) val author = getOrCreateUser(event.pubKey) if (version.event == null) { @@ -735,7 +747,7 @@ object LocalCache { getOrCreateChannel(note.idHex) { LiveActivitiesChannel(note.address) } as? LiveActivitiesChannel - val creator = event.host()?.ifBlank { null }?.let { checkGetOrCreateUser(it) } ?: author + val creator = event.host()?.let { checkGetOrCreateUser(it.pubKey) } ?: author channel?.updateChannelInfo(creator, event, event.createdAt) @@ -888,7 +900,7 @@ object LocalCache { relay: Relay?, ) { val version = getOrCreateNote(event.id) - val note = getOrCreateAddressableNote(event.address()) + val note = getOrCreateAddressableNote(event.aTag()) val author = getOrCreateUser(event.pubKey) if (version.event == null) { @@ -945,7 +957,7 @@ object LocalCache { fun consume(event: BadgeProfilesEvent) { val version = getOrCreateNote(event.id) - val note = getOrCreateAddressableNote(event.address()) + val note = getOrCreateAddressableNote(event.aTag()) val author = getOrCreateUser(event.pubKey) if (version.event == null) { @@ -1017,7 +1029,7 @@ object LocalCache { relay: Relay?, ) { val version = getOrCreateNote(event.id) - val note = getOrCreateAddressableNote(event.address()) + val note = getOrCreateAddressableNote(event.aTag()) val author = getOrCreateUser(event.pubKey) val replyTos = computeReplyTo(event) @@ -1182,7 +1194,7 @@ object LocalCache { getChannelIfExists(it.toTag())?.removeNote(deleteNote) } - (deletedEvent as? TorrentCommentEvent)?.torrent()?.let { + (deletedEvent as? TorrentCommentEvent)?.torrentIds()?.let { getNoteIfExists(it)?.removeReply(deleteNote) } @@ -1353,14 +1365,17 @@ object LocalCache { refreshObservers(note) } - fun consume(event: ChannelCreateEvent) { + fun consume( + event: ChannelCreateEvent, + relay: Relay?, + ) { // Log.d("MT", "New Event ${event.content} ${event.id.toHex()}") val oldChannel = getOrCreateChannel(event.id) { PublicChatChannel(it) } val author = getOrCreateUser(event.pubKey) val note = getOrCreateNote(event.id) if (note.event == null) { - oldChannel.addNote(note) + oldChannel.addNote(note, relay) note.loadEvent(event, author, emptyList()) refreshObservers(note) @@ -1371,13 +1386,16 @@ object LocalCache { } if (oldChannel.creator == null || oldChannel.creator == author) { if (oldChannel is PublicChatChannel) { - oldChannel.updateChannelInfo(author, event.channelInfo(), event.createdAt) + oldChannel.updateChannelInfo(author, event, event.createdAt) } } } - fun consume(event: ChannelMetadataEvent) { - val channelId = event.channel() + fun consume( + event: ChannelMetadataEvent, + relay: Relay?, + ) { + val channelId = event.channelId() // Log.d("MT", "New PublicChatMetadata ${event.channelInfo()}") if (channelId.isNullOrBlank()) return @@ -1396,7 +1414,7 @@ object LocalCache { val note = getOrCreateNote(event.id) if (note.event == null) { - oldChannel.addNote(note) + oldChannel.addNote(note, relay) note.loadEvent(event, author, emptyList()) refreshObservers(note) @@ -1407,14 +1425,14 @@ object LocalCache { event: ChannelMessageEvent, relay: Relay?, ) { - val channelId = event.channel() + val channelId = event.channelId() if (channelId.isNullOrBlank()) return val channel = checkGetOrCreateChannel(channelId) ?: return val note = getOrCreateNote(event.id) - channel.addNote(note) + channel.addNote(note, relay) val author = getOrCreateUser(event.pubKey) @@ -1457,7 +1475,7 @@ object LocalCache { val channel = getOrCreateChannel(activityId.toTag()) { LiveActivitiesChannel(activityId) } val note = getOrCreateNote(event.id) - channel.addNote(note) + channel.addNote(note, relay) val author = getOrCreateUser(event.pubKey) @@ -2102,7 +2120,7 @@ object LocalCache { val noteEvent = note.event if (noteEvent is AddressableEvent) { noteEvent.createdAt < - (addressables.get(noteEvent.address().toTag())?.event?.createdAt ?: 0) + (addressables.get(noteEvent.aTag().toTag())?.event?.createdAt ?: 0) } else { false } @@ -2111,7 +2129,7 @@ object LocalCache { val childrenToBeRemoved = mutableListOf() toBeRemoved.forEach { - val newerVersion = (it.event as? AddressableEvent)?.address()?.toTag()?.let { tag -> addressables.get(tag) } + val newerVersion = (it.event as? AddressableEvent)?.aTag()?.toTag()?.let { tag -> addressables.get(tag) } if (newerVersion != null) { it.moveAllReferencesTo(newerVersion) } @@ -2295,7 +2313,7 @@ object LocalCache { fun justVerify(event: Event): Boolean { checkNotInMainThread() - return if (!event.hasValidSignature()) { + return if (!event.verify()) { try { event.checkSignature() } catch (e: Exception) { @@ -2327,7 +2345,7 @@ object LocalCache { draftWrap: DraftEvent, draft: Event, ) { - val note = getOrCreateAddressableNote(draftWrap.address()) + val note = getOrCreateAddressableNote(draftWrap.aTag()) val author = getOrCreateUser(draftWrap.pubKey) when (draft) { @@ -2378,9 +2396,9 @@ object LocalCache { } } is ChannelMessageEvent -> { - draft.channel()?.let { channelId -> + draft.channelId()?.let { channelId -> checkGetOrCreateChannel(channelId)?.let { channel -> - channel.addNote(note) + channel.addNote(note, null) } } } @@ -2447,7 +2465,7 @@ object LocalCache { } } is ChannelMessageEvent -> { - draft.channel()?.let { channelId -> + draft.channelId()?.let { channelId -> checkGetOrCreateChannel(channelId)?.let { channel -> channel.removeNote(draftWrap) } @@ -2506,11 +2524,11 @@ object LocalCache { is CalendarDateSlotEvent -> consume(event, relay) is CalendarTimeSlotEvent -> consume(event, relay) is CalendarRSVPEvent -> consume(event, relay) - is ChannelCreateEvent -> consume(event) + is ChannelCreateEvent -> consume(event, relay) is ChannelListEvent -> consume(event, relay) is ChannelHideMessageEvent -> consume(event) is ChannelMessageEvent -> consume(event, relay) - is ChannelMetadataEvent -> consume(event) + is ChannelMetadataEvent -> consume(event, relay) is ChannelMuteUserEvent -> consume(event) is ChatMessageEncryptedFileHeaderEvent -> consume(event, relay) is ChatMessageEvent -> consume(event, relay) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt index 152031f67..3d370479c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -37,35 +37,40 @@ import com.vitorpamplona.ammolite.relays.BundledUpdate import com.vitorpamplona.ammolite.relays.Relay import com.vitorpamplona.ammolite.relays.RelayBriefInfoCache import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.quartz.experimental.bounties.addedRewardValue +import com.vitorpamplona.quartz.experimental.bounties.hasAdditionalReward import com.vitorpamplona.quartz.lightning.LnInvoiceUtil +import com.vitorpamplona.quartz.nip01Core.EventHintBundle import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag +import com.vitorpamplona.quartz.nip01Core.tags.events.EventReference import com.vitorpamplona.quartz.nip01Core.tags.hashtags.anyHashTag -import com.vitorpamplona.quartz.nip01Core.tags.hashtags.isTaggedHash import com.vitorpamplona.quartz.nip02FollowList.ImmutableListOfLists -import com.vitorpamplona.quartz.nip10Notes.BaseTextNoteEvent +import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent +import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent import com.vitorpamplona.quartz.nip19Bech32.toNAddr import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent -import com.vitorpamplona.quartz.nip28PublicChat.ChannelCreateEvent -import com.vitorpamplona.quartz.nip28PublicChat.ChannelMessageEvent -import com.vitorpamplona.quartz.nip28PublicChat.ChannelMetadataEvent +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent +import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitiveOrNSFW import com.vitorpamplona.quartz.nip37Drafts.DraftEvent import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentRequestEvent import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentResponseEvent import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceSuccessResponse -import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesChatMessageEvent -import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesEvent +import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent import com.vitorpamplona.quartz.nip59Giftwrap.WrappedEvent -import com.vitorpamplona.quartz.nip72ModCommunities.CommunityPostApprovalEvent +import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent import com.vitorpamplona.quartz.utils.Hex import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.containsAny @@ -108,6 +113,8 @@ class AddressableNote( val thisEvent = event return deletionAddressables.contains(address) || (thisEvent != null && deletionEvents.contains(thisEvent.id)) } + + override fun toATag() = ATag.parse(idHex, relayHintUrl()) } @Stable @@ -195,11 +202,11 @@ open class Note( event is LiveActivitiesChatMessageEvent || event is LiveActivitiesEvent ) { - (event as? ChannelMessageEvent)?.channel() - ?: (event as? ChannelMetadataEvent)?.channel() + (event as? ChannelMessageEvent)?.channelId() + ?: (event as? ChannelMetadataEvent)?.channelId() ?: (event as? ChannelCreateEvent)?.id ?: (event as? LiveActivitiesChatMessageEvent)?.activity()?.toTag() - ?: (event as? LiveActivitiesEvent)?.address()?.toTag() + ?: (event as? LiveActivitiesEvent)?.aTag()?.toTag() } else { null } @@ -442,6 +449,13 @@ open class Note( } } + fun addRelayBrief(brief: RelayBriefInfoCache.RelayBriefInfo) { + if (brief !in relays) { + addRelaySync(brief) + flowSet?.relays?.invalidateData() + } + } + private suspend fun isPaidByCalculation( account: Account, zapEvents: List>, @@ -702,7 +716,7 @@ open class Note( fun hasPledgeBy(user: User): Boolean = replies - .filter { it.event?.isTaggedHash("bounty-added-reward") ?: false } + .filter { it.event?.hasAdditionalReward() ?: false } .any { val pledgeValue = try { @@ -716,18 +730,7 @@ open class Note( pledgeValue != null && it.author == user } - fun pledgedAmountByOthers(): BigDecimal = - replies - .filter { it.event?.isTaggedHash("bounty-added-reward") ?: false } - .mapNotNull { - try { - BigDecimal(it.event?.content) - } catch (e: Exception) { - if (e is CancellationException) throw e - null - // do nothing if it can't convert to bigdecimal - } - }.sumOf { it } + fun pledgedAmountByOthers(): BigDecimal = replies.sumOf { it.event?.addedRewardValue() ?: BigDecimal.ZERO } fun hasAnyReports(): Boolean { val dayAgo = TimeUtils.oneDayAgo() @@ -839,7 +842,7 @@ open class Note( } if (accountChoices.hiddenWordsCase.isNotEmpty()) { - if (thisEvent is BaseTextNoteEvent && thisEvent.content.containsAny(accountChoices.hiddenWordsCase)) { + if (thisEvent is BaseThreadedEvent && thisEvent.content.containsAny(accountChoices.hiddenWordsCase)) { return true } @@ -915,7 +918,46 @@ open class Note( deletionAddressables: Set, ): Boolean { val thisEvent = event - return deletionEvents.contains(idHex) || (thisEvent is AddressableEvent && deletionAddressables.contains(thisEvent.address())) + return deletionEvents.contains(idHex) || (thisEvent is AddressableEvent && deletionAddressables.contains(thisEvent.aTag())) + } + + fun toETag(): ETag { + val noteEvent = event + return if (noteEvent != null) { + ETag(noteEvent.id, relayHintUrl(), noteEvent.pubKey) + } else { + ETag(idHex, relayHintUrl(), author?.pubkeyHex) + } + } + + fun toEId(): EventReference { + val noteEvent = event + return if (noteEvent != null) { + // uses the confirmed event id if available + EventReference(noteEvent.id, noteEvent.pubKey, relayHintUrl()) + } else { + EventReference(idHex, author?.pubkeyHex, relayHintUrl()) + } + } + + fun toEventHint() = (event as? T)?.let { EventHintBundle(it, relayHintUrl(), author?.bestRelayHint()) } + + fun toMarkedETag(marker: MarkedETag.MARKER): MarkedETag { + val noteEvent = event + return if (noteEvent != null) { + MarkedETag(noteEvent.id, relayHintUrl(), marker, noteEvent.pubKey) + } else { + MarkedETag(idHex, relayHintUrl(), marker, author?.pubkeyHex) + } + } + + open fun toATag(): ATag? { + val noteEvent = event + return if (noteEvent is AddressableEvent) { + ATag(noteEvent.kind, noteEvent.pubKey, noteEvent.dTag(), relayHintUrl()) + } else { + null + } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ThreadAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ThreadAssembler.kt index 0284c1704..b89437a33 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ThreadAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ThreadAssembler.kt @@ -142,7 +142,7 @@ class OnlyLatestVersionSet : MutableSet { return if (element is AddressableNote && loadedCreatedAt != null) { innerAdd(element.address, element, loadedCreatedAt) } else if (noteEvent is AddressableEvent && loadedCreatedAt != null) { - innerAdd(noteEvent.address(), element, loadedCreatedAt) + innerAdd(noteEvent.aTag(), element, loadedCreatedAt) } else { set.add(element) } @@ -193,7 +193,7 @@ class OnlyLatestVersionSet : MutableSet { element.address()?.let { map.remove(it) } - (element.event as? AddressableEvent)?.address()?.let { + (element.event as? AddressableEvent)?.aTag()?.let { map.remove(it) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt index 615c7e1d5..a07f3d3c5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt @@ -33,14 +33,15 @@ import com.vitorpamplona.ammolite.relays.Relay import com.vitorpamplona.ammolite.relays.filters.EOSETime import com.vitorpamplona.quartz.lightning.Lud06 import com.vitorpamplona.quartz.nip01Core.HexKey -import com.vitorpamplona.quartz.nip01Core.MetadataEvent -import com.vitorpamplona.quartz.nip01Core.UserMetadata +import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent +import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata import com.vitorpamplona.quartz.nip01Core.tags.geohash.isTaggedGeoHash import com.vitorpamplona.quartz.nip01Core.tags.hashtags.isTaggedHash +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import com.vitorpamplona.quartz.nip02FollowList.toImmutableListOfLists -import com.vitorpamplona.quartz.nip17Dm.ChatroomKey +import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile import com.vitorpamplona.quartz.nip19Bech32.toNpub import com.vitorpamplona.quartz.nip51Lists.BookmarkListEvent @@ -86,14 +87,15 @@ class User( fun pubkeyDisplayHex() = pubkeyNpub().toShortenHex() - fun toNProfile(): String { - val relayList = (LocalCache.getAddressableNoteIfExists(AdvertisedRelayListEvent.createAddressTag(pubkeyHex))?.event as? AdvertisedRelayListEvent)?.writeRelays() + fun authorRelayList() = (LocalCache.getAddressableNoteIfExists(AdvertisedRelayListEvent.createAddressTag(pubkeyHex))?.event as? AdvertisedRelayListEvent) - return NProfile.create( - pubkeyHex, - relayList?.take(3) ?: listOfNotNull(latestMetadataRelay), - ) - } + fun toNProfile() = NProfile.create(pubkeyHex, relayHints()) + + fun relayHints() = authorRelayList()?.writeRelays()?.take(3) ?: listOfNotNull(latestMetadataRelay) + + fun bestRelayHint() = authorRelayList()?.writeRelays()?.firstOrNull() ?: latestMetadataRelay + + fun toPTag() = PTag(pubkeyHex, bestRelayHint()) fun toNostrUri() = "nostr:${toNProfile()}" diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/AmethystNostrDataSource.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/AmethystNostrDataSource.kt index ff5312ab7..49185405b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/AmethystNostrDataSource.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/AmethystNostrDataSource.kt @@ -44,7 +44,7 @@ abstract class AmethystNostrDataSource( val note = LocalCache.getNoteIfExists(eventId) val noteEvent = note?.event if (noteEvent is AddressableEvent) { - LocalCache.getAddressableNoteIfExists(noteEvent.address().toTag())?.addRelay(relay) + LocalCache.getAddressableNoteIfExists(noteEvent.aTag().toTag())?.addRelay(relay) } else { note?.addRelay(relay) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt index 1d744cea0..7032f8761 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt @@ -36,20 +36,20 @@ import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStory import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStorySceneEvent import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent import com.vitorpamplona.quartz.nip01Core.HexKey -import com.vitorpamplona.quartz.nip01Core.MetadataEvent import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent -import com.vitorpamplona.quartz.nip17Dm.ChatMessageRelayListEvent +import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent -import com.vitorpamplona.quartz.nip28PublicChat.ChannelMessageEvent -import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiPackSelectionEvent -import com.vitorpamplona.quartz.nip34Git.GitIssueEvent -import com.vitorpamplona.quartz.nip34Git.GitPatchEvent -import com.vitorpamplona.quartz.nip34Git.GitReplyEvent +import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent +import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiPackSelectionEvent +import com.vitorpamplona.quartz.nip34Git.issue.GitIssueEvent +import com.vitorpamplona.quartz.nip34Git.patch.GitPatchEvent +import com.vitorpamplona.quartz.nip34Git.reply.GitReplyEvent import com.vitorpamplona.quartz.nip37Drafts.DraftEvent import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentResponseEvent @@ -64,12 +64,12 @@ import com.vitorpamplona.quartz.nip56Reports.ReportEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip58Badges.BadgeAwardEvent import com.vitorpamplona.quartz.nip58Badges.BadgeProfilesEvent -import com.vitorpamplona.quartz.nip59Giftwrap.GiftWrapEvent -import com.vitorpamplona.quartz.nip59Giftwrap.SealedRumorEvent +import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent +import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent -import com.vitorpamplona.quartz.nip96FileStorage.FileServersEvent +import com.vitorpamplona.quartz.nip96FileStorage.config.FileServersEvent import com.vitorpamplona.quartz.utils.TimeUtils // TODO: Migrate this to a property of AccountVi diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrChannelDataSource.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrChannelDataSource.kt index ec7320cbb..310b9d14f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrChannelDataSource.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrChannelDataSource.kt @@ -27,8 +27,8 @@ import com.vitorpamplona.amethyst.model.PublicChatChannel import com.vitorpamplona.ammolite.relays.FeedType import com.vitorpamplona.ammolite.relays.TypedFilter import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter -import com.vitorpamplona.quartz.nip28PublicChat.ChannelMessageEvent -import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesChatMessageEvent +import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent +import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent object NostrChannelDataSource : AmethystNostrDataSource("ChatroomFeed") { var account: Account? = null diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomDataSource.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomDataSource.kt index 0d2ca1d9e..2fd76043a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomDataSource.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomDataSource.kt @@ -26,7 +26,7 @@ import com.vitorpamplona.ammolite.relays.FeedType import com.vitorpamplona.ammolite.relays.TypedFilter import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter import com.vitorpamplona.quartz.nip04Dm.PrivateDmEvent -import com.vitorpamplona.quartz.nip17Dm.ChatroomKey +import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey object NostrChatroomDataSource : AmethystNostrDataSource("ChatroomFeed") { lateinit var account: Account diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomListDataSource.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomListDataSource.kt index 5e8f13eb0..8ddd310fa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomListDataSource.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomListDataSource.kt @@ -27,9 +27,9 @@ import com.vitorpamplona.ammolite.relays.FeedType import com.vitorpamplona.ammolite.relays.TypedFilter import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter import com.vitorpamplona.quartz.nip04Dm.PrivateDmEvent -import com.vitorpamplona.quartz.nip28PublicChat.ChannelCreateEvent -import com.vitorpamplona.quartz.nip28PublicChat.ChannelMessageEvent -import com.vitorpamplona.quartz.nip28PublicChat.ChannelMetadataEvent +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent +import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent object NostrChatroomListDataSource : AmethystNostrDataSource("MailBoxFeed") { lateinit var account: Account diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrCommunityDataSource.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrCommunityDataSource.kt index e57b4c3f9..a7c5ce451 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrCommunityDataSource.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrCommunityDataSource.kt @@ -24,8 +24,8 @@ import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES import com.vitorpamplona.ammolite.relays.TypedFilter import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter -import com.vitorpamplona.quartz.nip72ModCommunities.CommunityDefinitionEvent -import com.vitorpamplona.quartz.nip72ModCommunities.CommunityPostApprovalEvent +import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent +import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent object NostrCommunityDataSource : AmethystNostrDataSource("SingleCommunityFeed") { private var communityToWatch: AddressableNote? = null @@ -38,7 +38,7 @@ object NostrCommunityDataSource : AmethystNostrDataSource("SingleCommunityFeed") val authors = community .moderators() - .map { it.key } + .map { it.pubKey } .plus(listOfNotNull(myCommunityToWatch.author?.pubkeyHex)) if (authors.isEmpty()) return null diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrDiscoveryDataSource.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrDiscoveryDataSource.kt index 0e14b62bf..6fbad76ec 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrDiscoveryDataSource.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrDiscoveryDataSource.kt @@ -27,14 +27,14 @@ import com.vitorpamplona.ammolite.relays.FeedType import com.vitorpamplona.ammolite.relays.TypedFilter import com.vitorpamplona.ammolite.relays.filters.SinceAuthorPerRelayFilter import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter -import com.vitorpamplona.quartz.nip28PublicChat.ChannelCreateEvent -import com.vitorpamplona.quartz.nip28PublicChat.ChannelMessageEvent -import com.vitorpamplona.quartz.nip28PublicChat.ChannelMetadataEvent -import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesChatMessageEvent -import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesEvent -import com.vitorpamplona.quartz.nip72ModCommunities.CommunityDefinitionEvent -import com.vitorpamplona.quartz.nip72ModCommunities.CommunityPostApprovalEvent -import com.vitorpamplona.quartz.nip89AppHandlers.AppDefinitionEvent +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent +import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent +import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent +import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent +import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent +import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrGeohashDataSource.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrGeohashDataSource.kt index 413d9a4f7..dc8f1b73d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrGeohashDataSource.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrGeohashDataSource.kt @@ -23,13 +23,13 @@ package com.vitorpamplona.amethyst.service import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES import com.vitorpamplona.ammolite.relays.TypedFilter import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter -import com.vitorpamplona.quartz.experimental.audio.AudioHeaderEvent -import com.vitorpamplona.quartz.experimental.audio.AudioTrackEvent +import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent +import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent -import com.vitorpamplona.quartz.nip28PublicChat.ChannelMessageEvent +import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrHashtagDataSource.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrHashtagDataSource.kt index 9f81afd36..6e238a457 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrHashtagDataSource.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrHashtagDataSource.kt @@ -23,15 +23,15 @@ package com.vitorpamplona.amethyst.service import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES import com.vitorpamplona.ammolite.relays.TypedFilter import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter -import com.vitorpamplona.quartz.experimental.audio.AudioHeaderEvent -import com.vitorpamplona.quartz.experimental.audio.AudioTrackEvent +import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent +import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStorySceneEvent import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent -import com.vitorpamplona.quartz.nip28PublicChat.ChannelMessageEvent -import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesChatMessageEvent +import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent +import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt index ca6527a1f..a6c0d4066 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt @@ -27,22 +27,22 @@ import com.vitorpamplona.ammolite.relays.FeedType import com.vitorpamplona.ammolite.relays.TypedFilter import com.vitorpamplona.ammolite.relays.filters.SinceAuthorPerRelayFilter import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter -import com.vitorpamplona.quartz.experimental.audio.AudioHeaderEvent -import com.vitorpamplona.quartz.experimental.audio.AudioTrackEvent +import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent +import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent -import com.vitorpamplona.quartz.nip01Core.MetadataEvent +import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent import com.vitorpamplona.quartz.nip51Lists.PinListEvent -import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesChatMessageEvent -import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesEvent +import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent -import com.vitorpamplona.quartz.nip72ModCommunities.CommunityPostApprovalEvent +import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent import kotlinx.coroutines.Dispatchers diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrSearchEventOrUserDataSource.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrSearchEventOrUserDataSource.kt index b38c6ab1b..29b45f65a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrSearchEventOrUserDataSource.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrSearchEventOrUserDataSource.kt @@ -24,14 +24,14 @@ import com.vitorpamplona.ammolite.relays.ALL_FEED_TYPES import com.vitorpamplona.ammolite.relays.FeedType import com.vitorpamplona.ammolite.relays.TypedFilter import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter -import com.vitorpamplona.quartz.experimental.audio.AudioHeaderEvent -import com.vitorpamplona.quartz.experimental.audio.AudioTrackEvent +import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent +import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStorySceneEvent import com.vitorpamplona.quartz.experimental.nns.NNSEvent import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent -import com.vitorpamplona.quartz.nip01Core.KeyPair -import com.vitorpamplona.quartz.nip01Core.MetadataEvent +import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair +import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.toHexKey import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent @@ -44,19 +44,18 @@ import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile import com.vitorpamplona.quartz.nip19Bech32.entities.NPub import com.vitorpamplona.quartz.nip19Bech32.entities.NRelay import com.vitorpamplona.quartz.nip19Bech32.entities.NSec -import com.vitorpamplona.quartz.nip19Bech32.parse import com.vitorpamplona.quartz.nip22Comments.CommentEvent import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent -import com.vitorpamplona.quartz.nip28PublicChat.ChannelCreateEvent -import com.vitorpamplona.quartz.nip28PublicChat.ChannelMetadataEvent -import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiPackEvent +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent +import com.vitorpamplona.quartz.nip30CustomEmoji.pack.EmojiPackEvent import com.vitorpamplona.quartz.nip51Lists.BookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent import com.vitorpamplona.quartz.nip51Lists.PinListEvent -import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesEvent +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent import com.vitorpamplona.quartz.nip58Badges.BadgeDefinitionEvent -import com.vitorpamplona.quartz.nip72ModCommunities.CommunityDefinitionEvent +import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent import com.vitorpamplona.quartz.utils.Hex diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleChannelDataSource.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleChannelDataSource.kt index 39bbf9355..93894a463 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleChannelDataSource.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleChannelDataSource.kt @@ -27,8 +27,8 @@ import com.vitorpamplona.ammolite.relays.EVENT_FINDER_TYPES import com.vitorpamplona.ammolite.relays.FeedType import com.vitorpamplona.ammolite.relays.TypedFilter import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter -import com.vitorpamplona.quartz.nip28PublicChat.ChannelCreateEvent -import com.vitorpamplona.quartz.nip28PublicChat.ChannelMetadataEvent +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent object NostrSingleChannelDataSource : AmethystNostrDataSource("SingleChannelFeed") { private var channelsToWatch = setOf() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt index 1cc9266f9..a62e423dd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt @@ -35,12 +35,12 @@ import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent -import com.vitorpamplona.quartz.nip34Git.GitReplyEvent +import com.vitorpamplona.quartz.nip34Git.reply.GitReplyEvent import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent -import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesChatMessageEvent +import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.nip56Reports.ReportEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent -import com.vitorpamplona.quartz.nip72ModCommunities.CommunityPostApprovalEvent +import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryResponseEvent import com.vitorpamplona.quartz.nip90Dvms.NIP90StatusEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleUserDataSource.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleUserDataSource.kt index d79108c8a..8e0f28c1b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleUserDataSource.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleUserDataSource.kt @@ -26,8 +26,8 @@ import com.vitorpamplona.ammolite.relays.TypedFilter import com.vitorpamplona.ammolite.relays.filters.EOSETime import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter import com.vitorpamplona.quartz.experimental.relationshipStatus.RelationshipStatusEvent -import com.vitorpamplona.quartz.nip01Core.MetadataEvent -import com.vitorpamplona.quartz.nip17Dm.ChatMessageRelayListEvent +import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent +import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent import com.vitorpamplona.quartz.nip56Reports.ReportEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt index 9a7a9e033..f07dcfd24 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt @@ -24,12 +24,12 @@ import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES import com.vitorpamplona.ammolite.relays.TypedFilter import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter -import com.vitorpamplona.quartz.experimental.audio.AudioHeaderEvent -import com.vitorpamplona.quartz.experimental.audio.AudioTrackEvent +import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent +import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent -import com.vitorpamplona.quartz.nip01Core.MetadataEvent +import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent @@ -49,7 +49,7 @@ import com.vitorpamplona.quartz.nip68Picture.PictureEvent import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent -import com.vitorpamplona.quartz.nip89AppHandlers.AppRecommendationEvent +import com.vitorpamplona.quartz.nip89AppHandlers.recommendation.AppRecommendationEvent object NostrUserProfileDataSource : AmethystNostrDataSource("UserProfileFeed") { var user: User? = null diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrVideoDataSource.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrVideoDataSource.kt index 7fbb2f386..f0a0fa46c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrVideoDataSource.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrVideoDataSource.kt @@ -27,7 +27,7 @@ import com.vitorpamplona.ammolite.relays.FeedType import com.vitorpamplona.ammolite.relays.TypedFilter import com.vitorpamplona.ammolite.relays.filters.SinceAuthorPerRelayFilter import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter -import com.vitorpamplona.quartz.experimental.nip95.FileStorageHeaderEvent +import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent import com.vitorpamplona.quartz.nip68Picture.PictureEvent import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ZapPaymentHandler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ZapPaymentHandler.kt index 252f90d34..b089043fb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ZapPaymentHandler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ZapPaymentHandler.kt @@ -32,14 +32,14 @@ import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource.user import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceErrorResponse -import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesEvent +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip57Zaps.splits.BaseZapSplitSetup import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetup import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetupLnAddress import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplitSetup import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent -import com.vitorpamplona.quartz.nip89AppHandlers.AppDefinitionEvent +import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers @@ -77,7 +77,7 @@ class ZapPaymentHandler( if (!zapSplitSetup.isNullOrEmpty()) { zapSplitSetup } else if (noteEvent is LiveActivitiesEvent && noteEvent.hasHost()) { - noteEvent.hosts().map { ZapSplitSetup(it.pubKeyHex, it.relay, weight = 1.0) } + noteEvent.hosts().map { ZapSplitSetup(it.pubKey, it.relayHint, weight = 1.0) } } else if (noteEvent is AppDefinitionEvent) { val appLud16 = noteEvent.appMetaData()?.lnAddress() if (appLud16 != null) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt index bf6b5990a..991915cf5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt @@ -37,18 +37,18 @@ import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser -import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUsers +import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUserIds import com.vitorpamplona.quartz.nip04Dm.PrivateDmEvent -import com.vitorpamplona.quartz.nip17Dm.ChatMessageEncryptedFileHeaderEvent -import com.vitorpamplona.quartz.nip17Dm.ChatMessageEvent +import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent +import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent import com.vitorpamplona.quartz.nip19Bech32.toNpub import com.vitorpamplona.quartz.nip21UriScheme.toNostrUri import com.vitorpamplona.quartz.nip37Drafts.DraftEvent import com.vitorpamplona.quartz.nip55AndroidSigner.NostrSignerExternal import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent -import com.vitorpamplona.quartz.nip59Giftwrap.GiftWrapEvent -import com.vitorpamplona.quartz.nip59Giftwrap.SealedRumorEvent +import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent +import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import com.vitorpamplona.quartz.utils.TimeUtils import java.math.BigDecimal import kotlin.coroutines.cancellation.CancellationException @@ -137,7 +137,7 @@ class EventNotificationConsumer( Log.d(TAG, "New Notification Arrived") if (!LocalCache.justVerify(event)) return - val users = event.taggedUsers().map { LocalCache.getOrCreateUser(it) } + val users = event.taggedUserIds().map { LocalCache.getOrCreateUser(it) } val npubs = users.map { it.pubkeyNpub() }.toSet() // PushNotification Wraps don't include a receiver. diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/EncryptedBlobInterceptor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/EncryptedBlobInterceptor.kt index 07aa7e2a5..85447bfe1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/EncryptedBlobInterceptor.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/EncryptedBlobInterceptor.kt @@ -21,8 +21,8 @@ package com.vitorpamplona.amethyst.service.okhttp import android.util.Log -import com.vitorpamplona.quartz.nip17Dm.AESGCM -import com.vitorpamplona.quartz.nip17Dm.NostrCipher +import com.vitorpamplona.quartz.nip17Dm.files.encryption.AESGCM +import com.vitorpamplona.quartz.nip17Dm.files.encryption.NostrCipher import okhttp3.Interceptor import okhttp3.Response import okhttp3.ResponseBody.Companion.toResponseBody diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/EncryptionKeyCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/EncryptionKeyCache.kt index 190bb64ea..cd0fa1770 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/EncryptionKeyCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/EncryptionKeyCache.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.amethyst.service.okhttp import android.util.LruCache -import com.vitorpamplona.quartz.nip17Dm.NostrCipher +import com.vitorpamplona.quartz.nip17Dm.files.encryption.NostrCipher /** * Neigther ExoPlayer, nor Coil support passing key and nonce to the Interceptor via diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/HttpClientManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/HttpClientManager.kt index a3aae079a..85f418a1b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/HttpClientManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/HttpClientManager.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.amethyst.service.okhttp import android.util.Log -import com.vitorpamplona.quartz.nip17Dm.NostrCipher +import com.vitorpamplona.quartz.nip17Dm.files.encryption.NostrCipher import okhttp3.OkHttpClient import java.net.InetSocketAddress import java.net.Proxy diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpWebSocket.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpWebSocket.kt index 57c2fb6a7..f2f48552d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpWebSocket.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpWebSocket.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.service.okhttp -import com.vitorpamplona.quartz.nip01Core.relays.sockets.WebSocket -import com.vitorpamplona.quartz.nip01Core.relays.sockets.WebSocketListener -import com.vitorpamplona.quartz.nip01Core.relays.sockets.WebsocketBuilder -import com.vitorpamplona.quartz.nip01Core.relays.sockets.WebsocketBuilderFactory +import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebSocket +import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebSocketListener +import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebsocketBuilder +import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebsocketBuilderFactory import okhttp3.Request import okhttp3.Response diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/EncryptFiles.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/EncryptFiles.kt index 954e9b362..353e5d3cd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/EncryptFiles.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/EncryptFiles.kt @@ -25,7 +25,7 @@ import android.net.Uri import androidx.core.net.toUri import com.vitorpamplona.quartz.CryptoUtils import com.vitorpamplona.quartz.nip01Core.toHexKey -import com.vitorpamplona.quartz.nip17Dm.NostrCipher +import com.vitorpamplona.quartz.nip17Dm.files.encryption.NostrCipher import java.io.File class EncryptFilesResult( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/FileHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/FileHeader.kt index 3ef79122d..092bcdc22 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/FileHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/FileHeader.kt @@ -32,7 +32,7 @@ import com.vitorpamplona.amethyst.commons.blurhash.toBlurhash import com.vitorpamplona.amethyst.service.Blurhash import com.vitorpamplona.quartz.CryptoUtils import com.vitorpamplona.quartz.nip01Core.toHexKey -import com.vitorpamplona.quartz.nip94FileMetadata.Dimension +import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag import kotlinx.coroutines.CancellationException import java.io.IOException @@ -40,7 +40,7 @@ class FileHeader( val mimeType: String?, val hash: String, val size: Int, - val dim: Dimension?, + val dim: DimensionTag?, val blurHash: Blurhash?, ) { class UnableToDownload( @@ -51,7 +51,7 @@ class FileHeader( suspend fun prepare( fileUrl: String, mimeType: String?, - dimPrecomputed: Dimension?, + dimPrecomputed: DimensionTag?, forceProxy: Boolean, ): Result = try { @@ -71,7 +71,7 @@ class FileHeader( fun prepare( data: ByteArray, mimeType: String?, - dimPrecomputed: Dimension?, + dimPrecomputed: DimensionTag?, ): Result = try { val hash = CryptoUtils.sha256(data).toHexKey() @@ -82,7 +82,7 @@ class FileHeader( val opt = BitmapFactory.Options() opt.inPreferredConfig = Bitmap.Config.ARGB_8888 val mBitmap = BitmapFactory.decodeByteArray(data, 0, data.size, opt) - Pair(Blurhash(mBitmap.toBlurhash()), Dimension(mBitmap.width, mBitmap.height)) + Pair(Blurhash(mBitmap.toBlurhash()), DimensionTag(mBitmap.width, mBitmap.height)) } else if (mimeType?.startsWith("video/") == true) { val mediaMetadataRetriever = MediaMetadataRetriever() mediaMetadataRetriever.setDataSource(ByteArrayMediaDataSource(data)) @@ -135,12 +135,12 @@ fun MediaMetadataRetriever.getThumbnail(): Bitmap? { } } -fun MediaMetadataRetriever.prepareDimFromVideo(): Dimension? { +fun MediaMetadataRetriever.prepareDimFromVideo(): DimensionTag? { val width = prepareVideoWidth() ?: return null val height = prepareVideoHeight() ?: return null return if (width > 0 && height > 0) { - Dimension(width, height) + DimensionTag(width, height) } else { null } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MediaUploadResult.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MediaUploadResult.kt index a3a3a7dc0..39a35280e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MediaUploadResult.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MediaUploadResult.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.amethyst.service.uploads import com.vitorpamplona.quartz.nip01Core.HexKey -import com.vitorpamplona.quartz.nip94FileMetadata.Dimension +import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag data class MediaUploadResult( // A publicly accessible URL to the BUD-01 GET / endpoint (optionally with a file extension) @@ -35,7 +35,7 @@ data class MediaUploadResult( // upload time val uploaded: Long? = null, // dimensions - val dimension: Dimension? = null, + val dimension: DimensionTag? = null, // magnet link val magnet: String? = null, // info hash diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MultiOrchestrator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MultiOrchestrator.kt index 16da96e91..27ee4b75e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MultiOrchestrator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MultiOrchestrator.kt @@ -26,7 +26,7 @@ import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing -import com.vitorpamplona.quartz.nip17Dm.NostrCipher +import com.vitorpamplona.quartz.nip17Dm.files.encryption.NostrCipher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.joinAll @@ -49,7 +49,7 @@ class MultiOrchestrator( suspend fun upload( scope: CoroutineScope, alt: String?, - sensitiveContent: Boolean, + contentWarningReason: String?, mediaQuality: CompressorQuality, server: ServerName, account: Account, @@ -62,7 +62,7 @@ class MultiOrchestrator( item.media.uri, item.media.mimeType, alt, - sensitiveContent, + contentWarningReason, mediaQuality, server, account, @@ -79,7 +79,7 @@ class MultiOrchestrator( suspend fun uploadEncrypted( scope: CoroutineScope, alt: String?, - sensitiveContent: Boolean, + contentWarningReason: String?, mediaQuality: CompressorQuality, cipher: NostrCipher, server: ServerName, @@ -93,7 +93,7 @@ class MultiOrchestrator( item.media.uri, item.media.mimeType, alt, - sensitiveContent, + contentWarningReason, mediaQuality, cipher, server, @@ -126,5 +126,5 @@ class MultiOrchestrator( fun size() = list.size - fun get(index: Int) = list.get(index) + fun get(index: Int) = list[index] } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/UploadOrchestrator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/UploadOrchestrator.kt index b5d41763c..bbb09c70f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/UploadOrchestrator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/UploadOrchestrator.kt @@ -29,7 +29,7 @@ import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomUploader import com.vitorpamplona.amethyst.service.uploads.nip96.Nip96Uploader import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType -import com.vitorpamplona.quartz.nip17Dm.NostrCipher +import com.vitorpamplona.quartz.nip17Dm.files.encryption.NostrCipher import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.map import kotlin.coroutines.cancellation.CancellationException @@ -132,7 +132,7 @@ class UploadOrchestrator { contentType: String?, size: Long?, alt: String?, - sensitiveContent: Boolean, + contentWarningReason: String?, serverBaseUrl: String, contentTypeForResult: String?, originalHash: String?, @@ -147,7 +147,7 @@ class UploadOrchestrator { contentType = contentType, size = size, alt = alt, - sensitiveContent = if (sensitiveContent) "" else null, + sensitiveContent = contentWarningReason, serverBaseUrl = serverBaseUrl, forceProxy = account::shouldUseTorForNIP96, onProgress = { percent: Float -> @@ -175,7 +175,7 @@ class UploadOrchestrator { contentType: String?, size: Long?, alt: String?, - sensitiveContent: Boolean, + contentWarningReason: String?, serverBaseUrl: String, contentTypeForResult: String?, originalHash: String?, @@ -191,7 +191,7 @@ class UploadOrchestrator { contentType = contentType, size = size, alt = alt, - sensitiveContent = if (sensitiveContent) "" else null, + sensitiveContent = contentWarningReason, serverBaseUrl = serverBaseUrl, forceProxy = account::shouldUseTorForNIP96, httpAuth = account::createBlossomUploadAuth, @@ -292,7 +292,7 @@ class UploadOrchestrator { uri: Uri, mimeType: String?, alt: String?, - sensitiveContent: Boolean, + contentWarningReason: String?, compressionQuality: CompressorQuality, server: ServerName, account: Account, @@ -302,8 +302,8 @@ class UploadOrchestrator { return when (server.type) { ServerType.NIP95 -> uploadNIP95(compressed.uri, compressed.contentType, null, null, context) - ServerType.NIP96 -> uploadNIP96(compressed.uri, compressed.contentType, compressed.size, alt, sensitiveContent, server.baseUrl, null, null, account, context) - ServerType.Blossom -> uploadBlossom(compressed.uri, compressed.contentType, compressed.size, alt, sensitiveContent, server.baseUrl, null, null, account, context) + ServerType.NIP96 -> uploadNIP96(compressed.uri, compressed.contentType, compressed.size, alt, contentWarningReason, server.baseUrl, null, null, account, context) + ServerType.Blossom -> uploadBlossom(compressed.uri, compressed.contentType, compressed.size, alt, contentWarningReason, server.baseUrl, null, null, account, context) } } @@ -311,7 +311,7 @@ class UploadOrchestrator { uri: Uri, mimeType: String?, alt: String?, - sensitiveContent: Boolean, + contentWarningReason: String?, compressionQuality: CompressorQuality, encrypt: NostrCipher, server: ServerName, @@ -323,8 +323,8 @@ class UploadOrchestrator { return when (server.type) { ServerType.NIP95 -> uploadNIP95(encrypted.uri, encrypted.contentType, compressed.contentType, encrypted.originalHash, context) - ServerType.NIP96 -> uploadNIP96(encrypted.uri, encrypted.contentType, encrypted.size, alt, sensitiveContent, server.baseUrl, compressed.contentType, encrypted.originalHash, account, context) - ServerType.Blossom -> uploadBlossom(encrypted.uri, encrypted.contentType, encrypted.size, alt, sensitiveContent, server.baseUrl, compressed.contentType, encrypted.originalHash, account, context) + ServerType.NIP96 -> uploadNIP96(encrypted.uri, encrypted.contentType, encrypted.size, alt, contentWarningReason, server.baseUrl, compressed.contentType, encrypted.originalHash, account, context) + ServerType.Blossom -> uploadBlossom(encrypted.uri, encrypted.contentType, encrypted.size, alt, contentWarningReason, server.baseUrl, compressed.contentType, encrypted.originalHash, account, context) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/nip96/Nip96Uploader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/nip96/Nip96Uploader.kt index 7d1ad2e08..1ccc4b3a0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/nip96/Nip96Uploader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/nip96/Nip96Uploader.kt @@ -34,13 +34,12 @@ import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager import com.vitorpamplona.amethyst.service.uploads.MediaUploadResult import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.quartz.nip36SensitiveContent.CONTENT_WARNING -import com.vitorpamplona.quartz.nip94FileMetadata.Dimension -import com.vitorpamplona.quartz.nip96FileStorage.AuthToken -import com.vitorpamplona.quartz.nip96FileStorage.Nip96Result -import com.vitorpamplona.quartz.nip96FileStorage.PartialEvent -import com.vitorpamplona.quartz.nip96FileStorage.ResultParser -import com.vitorpamplona.quartz.nip96FileStorage.ServerInfo +import com.vitorpamplona.quartz.nip36SensitiveContent.ContentWarningTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag +import com.vitorpamplona.quartz.nip96FileStorage.actions.DeleteResult +import com.vitorpamplona.quartz.nip96FileStorage.actions.PartialEvent +import com.vitorpamplona.quartz.nip96FileStorage.actions.UploadResult +import com.vitorpamplona.quartz.nip96FileStorage.info.ServerInfo import com.vitorpamplona.quartz.nip98HttpAuth.HTTPAuthorizationEvent import kotlinx.coroutines.delay import okhttp3.MediaType.Companion.toMediaType @@ -153,7 +152,7 @@ class Nip96Uploader { .addFormDataPart("size", length.toString()) .also { body -> alt?.ifBlank { null }?.let { body.addFormDataPart("alt", it) } - sensitiveContent?.let { body.addFormDataPart(CONTENT_WARNING, it) } + sensitiveContent?.let { body.addFormDataPart(ContentWarningTag.TAG_NAME, it) } contentType?.let { body.addFormDataPart("content_type", it) } }.addFormDataPart( "file", @@ -169,7 +168,7 @@ class Nip96Uploader { }, ).build() - httpAuth(server.apiUrl, "POST", null)?.let { requestBuilder.addHeader("Authorization", AuthToken().encodeAuth(it)) } + httpAuth(server.apiUrl, "POST", null)?.let { requestBuilder.addHeader("Authorization", it.toAuthToken()) } requestBuilder .addHeader("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}") @@ -181,7 +180,7 @@ class Nip96Uploader { client.newCall(request).execute().use { response -> if (response.isSuccessful) { response.body.use { body -> - val result = ResultParser().parseResults(body.string()) + val result = UploadResult.parse(body.string()) if (!result.processingUrl.isNullOrBlank()) { return waitProcessing(result, server, forceProxy, onProgress) } else if (result.status == "success") { @@ -244,7 +243,7 @@ class Nip96Uploader { ?.firstOrNull { it.size > 1 && it[0] == "dim" } ?.get(1) ?.ifBlank { null } - ?.let { Dimension.parse(it) } + ?.let { DimensionTag.parse(it) } val magnet = nip96.tags ?.firstOrNull { it.size > 1 && it[0] == "magnet" } @@ -265,7 +264,7 @@ class Nip96Uploader { contentType: String?, server: ServerInfo, forceProxy: (String) -> Boolean, - httpAuth: (String, String, ByteArray?) -> HTTPAuthorizationEvent, + httpAuth: (String, String, ByteArray?) -> HTTPAuthorizationEvent?, context: Context, ): Boolean { val extension = @@ -275,7 +274,7 @@ class Nip96Uploader { val requestBuilder = Request.Builder() - httpAuth(server.apiUrl, "DELETE", null)?.let { requestBuilder.addHeader("Authorization", AuthToken().encodeAuth(it)) } + httpAuth(server.apiUrl, "DELETE", null)?.let { requestBuilder.addHeader("Authorization", it.toAuthToken()) } val request = requestBuilder @@ -287,7 +286,7 @@ class Nip96Uploader { client.newCall(request).execute().use { response -> if (response.isSuccessful) { response.body.use { body -> - val result = ResultParser().parseDeleteResults(body.string()) + val result = DeleteResult.parse(body.string()) return result.status == "success" } } else { @@ -302,7 +301,7 @@ class Nip96Uploader { } private suspend fun waitProcessing( - result: Nip96Result, + result: UploadResult, server: ServerInfo, forceProxy: (String) -> Boolean, onProgress: (percentage: Float) -> Unit, @@ -324,7 +323,7 @@ class Nip96Uploader { val client = HttpClientManager.getHttpClient(forceProxy(procUrl)) client.newCall(request).execute().use { if (it.isSuccessful) { - it.body.use { currentResult = ResultParser().parseResults(it.string()) } + it.body.use { currentResult = UploadResult.parse(it.string()) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/nip96/ServerInfoRetriever.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/nip96/ServerInfoRetriever.kt index b100bf1fb..d95119a31 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/nip96/ServerInfoRetriever.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/nip96/ServerInfoRetriever.kt @@ -23,8 +23,8 @@ package com.vitorpamplona.amethyst.service.uploads.nip96 import android.util.Log import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager -import com.vitorpamplona.quartz.nip96FileStorage.ServerInfo -import com.vitorpamplona.quartz.nip96FileStorage.ServerInfoParser +import com.vitorpamplona.quartz.nip96FileStorage.info.ServerInfo +import com.vitorpamplona.quartz.nip96FileStorage.info.ServerInfoParser import kotlinx.coroutines.CancellationException import okhttp3.Request diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt index 6d6902cd3..ed4f4bee3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt @@ -55,12 +55,12 @@ import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile import com.vitorpamplona.quartz.nip19Bech32.entities.NPub import com.vitorpamplona.quartz.nip19Bech32.entities.Note -import com.vitorpamplona.quartz.nip28PublicChat.ChannelCreateEvent -import com.vitorpamplona.quartz.nip28PublicChat.ChannelMessageEvent -import com.vitorpamplona.quartz.nip28PublicChat.ChannelMetadataEvent +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent +import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect -import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesEvent -import com.vitorpamplona.quartz.nip72ModCommunities.CommunityDefinitionEvent +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent +import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent import kotlinx.coroutines.CancellationException import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt index a9f56d0f6..aac0d7296 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt @@ -46,10 +46,19 @@ import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.ammolite.relays.RelaySetupInfo -import com.vitorpamplona.quartz.experimental.nip95.FileStorageEvent -import com.vitorpamplona.quartz.experimental.nip95.FileStorageHeaderEvent +import com.vitorpamplona.quartz.experimental.nip95.data.FileStorageEvent +import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent import com.vitorpamplona.quartz.nip92IMeta.IMetaTag import com.vitorpamplona.quartz.nip92IMeta.IMetaTagBuilder +import com.vitorpamplona.quartz.nip94FileMetadata.alt +import com.vitorpamplona.quartz.nip94FileMetadata.blurhash +import com.vitorpamplona.quartz.nip94FileMetadata.dims +import com.vitorpamplona.quartz.nip94FileMetadata.hash +import com.vitorpamplona.quartz.nip94FileMetadata.magnet +import com.vitorpamplona.quartz.nip94FileMetadata.mimeType +import com.vitorpamplona.quartz.nip94FileMetadata.originalHash +import com.vitorpamplona.quartz.nip94FileMetadata.sensitiveContent +import com.vitorpamplona.quartz.nip94FileMetadata.size import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -165,7 +174,7 @@ open class EditPostViewModel : ViewModel() { myMultiOrchestrator.upload( viewModelScope, alt, - sensitiveContent, + if (sensitiveContent) "" else null, MediaCompressor.intToCompressorQuality(mediaQuality), server, myAccount, @@ -175,7 +184,12 @@ open class EditPostViewModel : ViewModel() { if (results.allGood) { results.successful.forEach { state -> if (state.result is UploadOrchestrator.OrchestratorResult.NIP95Result) { - account?.createNip95(state.result.bytes, headerInfo = state.result.fileHeader, alt, sensitiveContent) { nip95 -> + account?.createNip95( + state.result.bytes, + headerInfo = state.result.fileHeader, + alt, + if (sensitiveContent) "" else null, + ) { nip95 -> nip95attachments = nip95attachments + nip95 val note = nip95.let { it1 -> account?.consumeNip95(it1.first, it1.second) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewChannelViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewChannelViewModel.kt index 25b42c4fe..e8278ce56 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewChannelViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewChannelViewModel.kt @@ -26,6 +26,10 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.PublicChatChannel +import com.vitorpamplona.quartz.nip01Core.EventHintBundle +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -53,19 +57,44 @@ class NewChannelViewModel : ViewModel() { fun create() { viewModelScope.launch(Dispatchers.IO) { account?.let { account -> - if (originalChannel == null) { - account.sendCreateNewChannel( - channelName.value.text, - channelDescription.value.text, - channelPicture.value.text, - ) + val channel = originalChannel + if (channel == null) { + val template = + ChannelCreateEvent.build( + channelName.value.text, + channelDescription.value.text, + channelPicture.value.text, + null, + ) + + account.sendCreateNewChannel(template) } else { - account.sendChangeChannel( - channelName.value.text, - channelDescription.value.text, - channelPicture.value.text, - originalChannel!!, - ) + val event = channel.event + + val template = + if (event != null) { + val hint = EventHintBundle(event, channel.relays().firstOrNull()) + + ChannelMetadataEvent.build( + channelName.value.text, + channelDescription.value.text, + channelPicture.value.text, + null, + hint, + ) + } else { + val eTag = ETag(channel.idHex, channel.relays().firstOrNull()) + + ChannelMetadataEvent.build( + channelName.value.text, + channelDescription.value.text, + channelPicture.value.text, + null, + eTag, + ) + } + + account.sendChangeChannel(template) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt index f818db786..2f096d38e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt @@ -99,7 +99,7 @@ open class NewMediaModel : ViewModel() { myMultiOrchestrator.upload( viewModelScope, caption, - sensitiveContent, + if (sensitiveContent) "" else null, MediaCompressor.intToCompressorQuality(mediaQualitySlider), serverToUse, myAccount, @@ -140,7 +140,7 @@ open class NewMediaModel : ViewModel() { viewModelScope.launch(Dispatchers.IO) { withTimeoutOrNull(30000) { suspendCancellableCoroutine { continuation -> - account?.createNip95(it.bytes, headerInfo = it.fileHeader, caption, sensitiveContent) { nip95 -> + account?.createNip95(it.bytes, headerInfo = it.fileHeader, caption, if (sensitiveContent) "" else null) { nip95 -> account?.consumeAndSendNip95(nip95.first, nip95.second, relayList) continuation.resume(true) } @@ -160,7 +160,7 @@ open class NewMediaModel : ViewModel() { it.magnet, it.fileHeader, caption, - sensitiveContent, + if (sensitiveContent) "" else null, it.uploadedHash, relayList, ) { @@ -180,7 +180,7 @@ open class NewMediaModel : ViewModel() { account?.sendAllAsOnePictureEvent( imageUrls, caption, - sensitiveContent, + if (sensitiveContent) "" else null, relayList, ) { continuation.resume(true) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt index 3f613da33..41816ec2a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt @@ -24,7 +24,7 @@ import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.quartz.nip01Core.HexKey -import com.vitorpamplona.quartz.nip01Core.KeyPair +import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser import com.vitorpamplona.quartz.nip19Bech32.bech32.Bech32 import com.vitorpamplona.quartz.nip19Bech32.bech32.bechToBytes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollClosing.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollClosing.kt index 8c8a61c20..0f146f7b7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollClosing.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollClosing.kt @@ -52,7 +52,7 @@ fun NewPollClosing(pollViewModel: NewPostViewModel) { pollViewModel.isValidClosedAt.value = true if (text.isNotEmpty()) { try { - val int = text.toInt() + val int = text.toLong() if (int < 0) { pollViewModel.isValidClosedAt.value = false } else { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt index d92db895f..ac3fa4b89 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt @@ -38,9 +38,12 @@ import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.compose.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.richtext.RichTextParser +import com.vitorpamplona.amethyst.commons.richtext.RichTextParser.Companion.imageExtensions import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.LiveActivitiesChannel import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.PublicChatChannel import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.LocationState import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource @@ -55,42 +58,84 @@ import com.vitorpamplona.amethyst.ui.components.Split import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.ammolite.relays.RelaySetupInfo -import com.vitorpamplona.quartz.experimental.nip95.FileStorageEvent -import com.vitorpamplona.quartz.experimental.nip95.FileStorageHeaderEvent +import com.vitorpamplona.quartz.experimental.nip95.data.FileStorageEvent +import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent +import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent +import com.vitorpamplona.quartz.experimental.zapPolls.closedAt +import com.vitorpamplona.quartz.experimental.zapPolls.consensusThreshold +import com.vitorpamplona.quartz.experimental.zapPolls.maxAmount +import com.vitorpamplona.quartz.experimental.zapPolls.minAmount +import com.vitorpamplona.quartz.experimental.zapPolls.tags.PollOptionTag +import com.vitorpamplona.quartz.nip01Core.EventHintBundle import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag +import com.vitorpamplona.quartz.nip01Core.tags.events.eTags +import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohash import com.vitorpamplona.quartz.nip01Core.tags.geohash.getGeoHash +import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags +import com.vitorpamplona.quartz.nip01Core.tags.people.pTags +import com.vitorpamplona.quartz.nip01Core.tags.references.references import com.vitorpamplona.quartz.nip04Dm.PrivateDmEvent -import com.vitorpamplona.quartz.nip10Notes.BaseTextNoteEvent +import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent +import com.vitorpamplona.quartz.nip10Notes.content.findHashtags +import com.vitorpamplona.quartz.nip10Notes.content.findNostrUris import com.vitorpamplona.quartz.nip10Notes.content.findURLs +import com.vitorpamplona.quartz.nip10Notes.tags.notify +import com.vitorpamplona.quartz.nip10Notes.tags.positionalMarkedTags import com.vitorpamplona.quartz.nip14Subject.subject -import com.vitorpamplona.quartz.nip17Dm.NIP17Group +import com.vitorpamplona.quartz.nip17Dm.base.NIP17Group +import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent +import com.vitorpamplona.quartz.nip17Dm.messages.changeSubject +import com.vitorpamplona.quartz.nip18Reposts.quotes.quotes import com.vitorpamplona.quartz.nip19Bech32.toNpub import com.vitorpamplona.quartz.nip22Comments.CommentEvent import com.vitorpamplona.quartz.nip22Comments.RootScope +import com.vitorpamplona.quartz.nip22Comments.notify +import com.vitorpamplona.quartz.nip28PublicChat.base.notify +import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent import com.vitorpamplona.quartz.nip30CustomEmoji.CustomEmoji -import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrl -import com.vitorpamplona.quartz.nip34Git.GitIssueEvent +import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag +import com.vitorpamplona.quartz.nip30CustomEmoji.emojis +import com.vitorpamplona.quartz.nip34Git.issue.GitIssueEvent +import com.vitorpamplona.quartz.nip34Git.reply.GitReplyEvent +import com.vitorpamplona.quartz.nip34Git.reply.notify import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent +import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitive import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitiveOrNSFW import com.vitorpamplona.quartz.nip37Drafts.DraftEvent +import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent +import com.vitorpamplona.quartz.nip53LiveActivities.chat.notify import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetup import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetupLnAddress import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplitSetup +import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplits +import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiser import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent -import com.vitorpamplona.quartz.nip72ModCommunities.CommunityDefinitionEvent +import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent +import com.vitorpamplona.quartz.nip73ExternalIds.GeohashId import com.vitorpamplona.quartz.nip92IMeta.IMetaTag import com.vitorpamplona.quartz.nip92IMeta.IMetaTagBuilder +import com.vitorpamplona.quartz.nip92IMeta.imetas +import com.vitorpamplona.quartz.nip94FileMetadata.alt +import com.vitorpamplona.quartz.nip94FileMetadata.blurhash +import com.vitorpamplona.quartz.nip94FileMetadata.dims +import com.vitorpamplona.quartz.nip94FileMetadata.hash +import com.vitorpamplona.quartz.nip94FileMetadata.magnet +import com.vitorpamplona.quartz.nip94FileMetadata.mimeType +import com.vitorpamplona.quartz.nip94FileMetadata.originalHash +import com.vitorpamplona.quartz.nip94FileMetadata.sensitiveContent +import com.vitorpamplona.quartz.nip94FileMetadata.size import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent -import com.vitorpamplona.quartz.nip99Classifieds.Price +import com.vitorpamplona.quartz.nip99Classifieds.tags.ConditionTag +import com.vitorpamplona.quartz.nip99Classifieds.tags.PriceTag import com.vitorpamplona.quartz.utils.Hex import kotlinx.collections.immutable.ImmutableList -import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow @@ -171,10 +216,10 @@ open class NewPostViewModel : ViewModel() { var wantsPoll by mutableStateOf(false) var zapRecipients = mutableStateListOf() var pollOptions = newStateMapPollOptions() - var valueMaximum by mutableStateOf(null) - var valueMinimum by mutableStateOf(null) + var valueMaximum by mutableStateOf(null) + var valueMinimum by mutableStateOf(null) var consensusThreshold: Int? = null - var closedAt: Int? = null + var closedAt: Long? = null var isValidRecipients = mutableStateOf(true) var isValidvalueMaximum = mutableStateOf(true) @@ -188,8 +233,7 @@ open class NewPostViewModel : ViewModel() { var price by mutableStateOf(TextFieldValue("")) var locationText by mutableStateOf(TextFieldValue("")) var category by mutableStateOf(TextFieldValue("")) - var condition by - mutableStateOf(ClassifiedsEvent.CONDITION.USED_LIKE_NEW) + var condition by mutableStateOf(ConditionTag.CONDITION.USED_LIKE_NEW) // Invoices var canAddInvoice by mutableStateOf(false) @@ -253,7 +297,7 @@ open class NewPostViewModel : ViewModel() { } else { originalNote = replyingTo replyingTo?.let { replyNote -> - if (replyNote.event is BaseTextNoteEvent) { + if (replyNote.event is BaseThreadedEvent) { this.eTags = (replyNote.replyTo ?: emptyList()).plus(replyNote) } else { this.eTags = listOf(replyNote) @@ -264,8 +308,7 @@ open class NewPostViewModel : ViewModel() { val currentMentions = (replyNote.event as? TextNoteEvent) ?.mentions() - ?.filter { it.isNotEmpty() } - ?.map { LocalCache.getOrCreateUser(it) } + ?.map { LocalCache.getOrCreateUser(it.pubKey) } ?: emptyList() if (currentMentions.contains(replyUser)) { @@ -283,7 +326,7 @@ open class NewPostViewModel : ViewModel() { canAddInvoice = accountViewModel.userProfile().info?.lnAddress() != null canAddZapRaiser = accountViewModel.userProfile().info?.lnAddress() != null - canUsePoll = originalNote?.event !is PrivateDmEvent && originalNote?.channelHex() == null + canUsePoll = originalNote == null multiOrchestrator = null quote?.let { @@ -431,7 +474,7 @@ open class NewPostViewModel : ViewModel() { }.firstOrNull() } - canUsePoll = originalNote?.event !is PrivateDmEvent && originalNote?.channelHex() == null + canUsePoll = originalNote == null if (forwardZapTo.items.isNotEmpty()) { wantsForwardZapTo = true @@ -447,9 +490,9 @@ open class NewPostViewModel : ViewModel() { val minMax = draftEvent.tags.filter { it.size > 1 && (it[0] == "value_minimum" || it[0] == "value_maximum") } minMax.forEach { if (it[0] == "value_maximum") { - valueMaximum = it[1].toInt() + valueMaximum = it[1].toLong() } else if (it[0] == "value_minimum") { - valueMinimum = it[1].toInt() + valueMinimum = it[1].toLong() } } @@ -487,14 +530,14 @@ open class NewPostViewModel : ViewModel() { .map { it[1] } ?.firstOrNull() ?: "", ) - condition = ClassifiedsEvent.CONDITION.entries.firstOrNull { + condition = ConditionTag.CONDITION.entries.firstOrNull { it.value == draftEvent .tags .filter { it.size > 1 && it[0] == "condition" } .map { it[1] } .firstOrNull() - } ?: ClassifiedsEvent.CONDITION.USED_LIKE_NEW + } ?: ConditionTag.CONDITION.USED_LIKE_NEW wantsDirectMessage = draftEvent is PrivateDmEvent || draftEvent is NIP17Group @@ -539,7 +582,11 @@ open class NewPostViewModel : ViewModel() { } suspend fun sendDraftSync(relayList: List) { - innerSendPost(relayList, draftTag) + if (message.text.isBlank()) { + account?.deleteDraft(draftTag) + } else { + innerSendPost(relayList, draftTag) + } } private suspend fun innerSendPost( @@ -594,67 +641,181 @@ open class NewPostViewModel : ViewModel() { val usedAttachments = iMetaAttachments.filter { it.url in urls.toSet() } val replyingTo = originalNote + val contentWarningReason = if (wantsToMarkAsSensitive) "" else null - if (replyingTo?.event is CommentEvent || (replyingTo?.event is Event && replyingTo.event is RootScope)) { - account?.sendReplyComment( - message = tagger.message, - replyingTo = replyingTo, - directMentionsUsers = tagger.directMentionsUsers, - directMentionsNotes = tagger.directMentionsNotes, - imetas = usedAttachments, - geohash = geoHash, - zapReceiver = zapReceiver, - wantsToMarkAsSensitive = wantsToMarkAsSensitive, - zapRaiserAmount = localZapRaiserAmount, - relayList = relayList, - emojis = emojis, - draftTag = localDraft, - ) - } else if (wantsExclusiveGeoPost && geoHash != null && (originalNote == null || originalNote?.event is CommentEvent)) { - account?.sendGeoComment( - message = tagger.message, - geohash = geoHash, - replyingTo = originalNote, - directMentionsUsers = tagger.directMentionsUsers, - directMentionsNotes = tagger.directMentionsNotes, - imetas = usedAttachments, - zapReceiver = zapReceiver, - wantsToMarkAsSensitive = wantsToMarkAsSensitive, - zapRaiserAmount = localZapRaiserAmount, - relayList = relayList, - emojis = emojis, - draftTag = localDraft, - ) - } else if (originalNote?.channelHex() != null) { - if (originalNote is AddressableEvent && originalNote?.address() != null) { - account?.sendLiveMessage( - message = tagger.message, - toChannel = originalNote?.address()!!, - replyTo = tagger.eTags, - mentions = tagger.pTags, - zapReceiver = zapReceiver, - wantsToMarkAsSensitive = wantsToMarkAsSensitive, - zapRaiserAmount = localZapRaiserAmount, - geohash = geoHash, - imetas = usedAttachments, - emojis = emojis, - draftTag = localDraft, - ) - } else { - account?.sendChannelMessage( - message = tagger.message, - toChannel = tagger.channelHex!!, - replyTo = tagger.eTags, - mentions = tagger.pTags, - zapReceiver = zapReceiver, - wantsToMarkAsSensitive = wantsToMarkAsSensitive, - zapRaiserAmount = localZapRaiserAmount, - directMentions = tagger.directMentions, - geohash = geoHash, - imetas = usedAttachments, - emojis = emojis, - draftTag = localDraft, - ) + val channel = originalNote?.channelHex()?.let { LocalCache.getChannelIfExists(it) } + + if (replyingTo?.event is CommentEvent || replyingTo?.event is RootScope) { + val eventHint = replyingTo.toEventHint() ?: return@withContext + + val template = + CommentEvent.replyBuilder( + msg = tagger.message, + replyingTo = eventHint, + ) { + tagger.pTags?.let { notify(it.map { it.toPTag() }) } + + hashtags(findHashtags(tagger.message)) + references(findURLs(tagger.message)) + quotes(findNostrUris(tagger.message)) + + geoHash?.let { geohash(it) } + zapRaiserAmount?.let { zapraiser(it) } + zapReceiver?.let { zapSplits(it) } + contentWarningReason?.let { contentWarning(it) } + + emojis(emojis) + imetas(usedAttachments) + } + + account?.signAndSend(localDraft, template, relayList, setOf(replyingTo)) + } else if (wantsExclusiveGeoPost && geoHash != null && originalNote == null) { + val template = + CommentEvent.replyExternalIdentity( + msg = tagger.message, + extId = GeohashId(geoHash), + ) { + tagger.pTags?.let { notify(it.map { it.toPTag() }) } + + hashtags(findHashtags(tagger.message)) + references(findURLs(tagger.message)) + quotes(findNostrUris(tagger.message)) + + zapRaiserAmount?.let { zapraiser(it) } + zapReceiver?.let { zapSplits(it) } + contentWarningReason?.let { contentWarning(it) } + + emojis(emojis) + imetas(usedAttachments) + } + + account?.signAndSend(localDraft, template, relayList, emptyList()) + } else if (channel != null) { + if (channel is PublicChatChannel) { + val replyingToEvent = originalNote?.toEventHint() + val channelEvent = channel.event + val channelRelays = channel.relays() + + val template = + if (replyingToEvent != null) { + ChannelMessageEvent.reply(tagger.message, replyingToEvent) { + tagger.pTags?.let { notify(it.map { it.toPTag() }) } + + hashtags(findHashtags(tagger.message)) + references(findURLs(tagger.message)) + quotes(findNostrUris(tagger.message)) + + geoHash?.let { geohash(it) } + localZapRaiserAmount?.let { zapraiser(it) } + zapReceiver?.let { zapSplits(it) } + contentWarningReason?.let { contentWarning(it) } + + emojis(emojis) + imetas(usedAttachments) + } + } else if (channelEvent != null) { + val hint = EventHintBundle(channelEvent, channelRelays.firstOrNull()) + ChannelMessageEvent.message(tagger.message, hint) { + tagger.pTags?.let { notify(it.map { it.toPTag() }) } + + hashtags(findHashtags(tagger.message)) + references(findURLs(tagger.message)) + quotes(findNostrUris(tagger.message)) + + geoHash?.let { geohash(it) } + localZapRaiserAmount?.let { zapraiser(it) } + zapReceiver?.let { zapSplits(it) } + contentWarningReason?.let { contentWarning(it) } + + emojis(emojis) + imetas(usedAttachments) + } + } else { + ChannelMessageEvent.message(tagger.message, ETag(channel.idHex, channelRelays.firstOrNull())) { + tagger.pTags?.let { notify(it.map { it.toPTag() }) } + + hashtags(findHashtags(tagger.message)) + references(findURLs(tagger.message)) + quotes(findNostrUris(tagger.message)) + + geoHash?.let { geohash(it) } + localZapRaiserAmount?.let { zapraiser(it) } + zapReceiver?.let { zapSplits(it) } + contentWarningReason?.let { contentWarning(it) } + + emojis(emojis) + imetas(usedAttachments) + } + } + + val broadcast = tagger.directMentionsNotes + (tagger.eTags ?: emptyList()) + + account?.signAndSendWithList(draftTag, template, channelRelays, broadcast) + } else if (channel is LiveActivitiesChannel) { + val replyingToEvent = originalNote?.toEventHint() + val activity = channel.info + val channelRelays = channel.relays() + + val template = + if (replyingToEvent != null) { + LiveActivitiesChatMessageEvent.reply(tagger.message, replyingToEvent) { + tagger.pTags?.let { notify(it.map { it.toPTag() }) } + + hashtags(findHashtags(tagger.message)) + references(findURLs(tagger.message)) + quotes(findNostrUris(tagger.message)) + + geoHash?.let { geohash(it) } + localZapRaiserAmount?.let { zapraiser(it) } + zapReceiver?.let { zapSplits(it) } + contentWarningReason?.let { contentWarning(it) } + + emojis(emojis) + imetas(usedAttachments) + } + } else if (activity != null) { + val hint = EventHintBundle(activity, channelRelays.firstOrNull() ?: replyingToEvent?.relay) + + LiveActivitiesChatMessageEvent.message(tagger.message, hint) { + tagger.pTags?.let { notify(it.map { it.toPTag() }) } + + hashtags(findHashtags(tagger.message)) + references(findURLs(tagger.message)) + quotes(findNostrUris(tagger.message)) + + geoHash?.let { geohash(it) } + localZapRaiserAmount?.let { zapraiser(it) } + zapReceiver?.let { zapSplits(it) } + contentWarningReason?.let { contentWarning(it) } + + emojis(emojis) + imetas(usedAttachments) + } + } else { + if (channel.address.relay == null) { + channel.address.relay = channelRelays.firstOrNull() ?: replyingToEvent?.relay + } + + LiveActivitiesChatMessageEvent.message(tagger.message, channel.address) { + tagger.pTags?.let { notify(it.map { it.toPTag() }) } + + hashtags(findHashtags(tagger.message)) + references(findURLs(tagger.message)) + quotes(findNostrUris(tagger.message)) + + geoHash?.let { geohash(it) } + localZapRaiserAmount?.let { zapraiser(it) } + zapReceiver?.let { zapSplits(it) } + contentWarningReason?.let { contentWarning(it) } + + emojis(emojis) + imetas(usedAttachments) + } + } + + val broadcast = tagger.directMentionsNotes + (tagger.eTags ?: emptyList()) + + account?.signAndSendWithList(draftTag, template, channelRelays, broadcast) } } else if (originalNote?.event is PrivateDmEvent) { account?.sendPrivateMessage( @@ -663,50 +824,98 @@ open class NewPostViewModel : ViewModel() { replyingTo = originalNote!!, mentions = tagger.pTags, zapReceiver = zapReceiver, - wantsToMarkAsSensitive = wantsToMarkAsSensitive, + contentWarningReason = contentWarningReason, zapRaiserAmount = localZapRaiserAmount, geohash = geoHash, imetas = usedAttachments, draftTag = localDraft, ) } else if (originalNote?.event is NIP17Group) { - account?.sendNIP17PrivateMessage( - message = tagger.message, - toUsers = (originalNote?.event as NIP17Group).groupMembers().toList(), - subject = subject.text.ifBlank { null }, - replyingTo = originalNote!!, - mentions = tagger.pTags, - wantsToMarkAsSensitive = wantsToMarkAsSensitive, - zapReceiver = zapReceiver, - zapRaiserAmount = localZapRaiserAmount, - geohash = geoHash, - imetas = usedAttachments, - emojis = emojis, - draftTag = localDraft, - ) + val replyHint = originalNote?.toEventHint() + + val template = + if (replyHint == null) { + val msgTo = (originalNote?.event as NIP17Group).groupMembers().map { LocalCache.getOrCreateUser(it).toPTag() } + ChatMessageEvent.build(tagger.message, msgTo) { + subject.text.ifBlank { null }?.let { changeSubject(it) } + + hashtags(findHashtags(tagger.message)) + references(findURLs(tagger.message)) + quotes(findNostrUris(tagger.message)) + + geoHash?.let { geohash(it) } + localZapRaiserAmount?.let { zapraiser(it) } + zapReceiver?.let { zapSplits(it) } + contentWarningReason?.let { contentWarning(it) } + + emojis(emojis) + imetas(usedAttachments) + } + } else { + ChatMessageEvent.reply(tagger.message, replyHint) { + subject.text.ifBlank { null }?.let { changeSubject(it) } + + hashtags(findHashtags(tagger.message)) + references(findURLs(tagger.message)) + quotes(findNostrUris(tagger.message)) + + geoHash?.let { geohash(it) } + localZapRaiserAmount?.let { zapraiser(it) } + zapReceiver?.let { zapSplits(it) } + contentWarningReason?.let { contentWarning(it) } + + emojis(emojis) + imetas(usedAttachments) + } + } + + account?.sendNIP17PrivateMessage(template, localDraft) } else if (!dmUsers.isNullOrEmpty()) { if (nip17 || dmUsers.size > 1) { - account?.sendNIP17PrivateMessage( - message = tagger.message, - toUsers = dmUsers.map { it.pubkeyHex }, - subject = subject.text.ifBlank { null }, - replyingTo = tagger.eTags?.firstOrNull(), - mentions = tagger.pTags, - wantsToMarkAsSensitive = wantsToMarkAsSensitive, - zapReceiver = zapReceiver, - zapRaiserAmount = localZapRaiserAmount, - geohash = geoHash, - imetas = usedAttachments, - emojis = emojis, - draftTag = localDraft, - ) + val replyHint = originalNote?.toEventHint() + val template = + if (replyHint == null) { + ChatMessageEvent.build(tagger.message, dmUsers.map { it.toPTag() }) { + subject.text.ifBlank { null }?.let { changeSubject(it) } + + hashtags(findHashtags(tagger.message)) + references(findURLs(tagger.message)) + quotes(findNostrUris(tagger.message)) + + geoHash?.let { geohash(it) } + localZapRaiserAmount?.let { zapraiser(it) } + zapReceiver?.let { zapSplits(it) } + contentWarningReason?.let { contentWarning(it) } + + emojis(emojis) + imetas(usedAttachments) + } + } else { + ChatMessageEvent.reply(tagger.message, replyHint) { + subject.text.ifBlank { null }?.let { changeSubject(it) } + + hashtags(findHashtags(tagger.message)) + references(findURLs(tagger.message)) + quotes(findNostrUris(tagger.message)) + + geoHash?.let { geohash(it) } + localZapRaiserAmount?.let { zapraiser(it) } + zapReceiver?.let { zapSplits(it) } + contentWarningReason?.let { contentWarning(it) } + + emojis(emojis) + imetas(usedAttachments) + } + } + + account?.sendNIP17PrivateMessage(template, localDraft) } else { account?.sendPrivateMessage( message = tagger.message, - toUser = dmUsers.first().pubkeyHex, + toUser = dmUsers.first(), replyingTo = originalNote, mentions = tagger.pTags, - wantsToMarkAsSensitive = wantsToMarkAsSensitive, + contentWarningReason = contentWarningReason, zapReceiver = zapReceiver, zapRaiserAmount = localZapRaiserAmount, geohash = geoHash, @@ -715,62 +924,50 @@ open class NewPostViewModel : ViewModel() { ) } } else if (originalNote?.event is GitIssueEvent) { - val originalNoteEvent = originalNote?.event as GitIssueEvent - // adds markers - val rootId = - originalNoteEvent.rootIssueOrPatch() // if it has a marker as root - ?: originalNote - ?.replyTo - ?.firstOrNull { it.event != null && it.replyTo?.isEmpty() == true } - ?.idHex // if it has loaded events with zero replies in the reply list - ?: originalNote?.replyTo?.firstOrNull()?.idHex // old rules, first item is root. - ?: originalNote?.idHex + val originalNoteHint = originalNote?.toEventHint() ?: return@withContext - val replyId = originalNote?.idHex + val template = + GitReplyEvent.replyIssue( + tagger.message, + originalNoteHint, + ) { + tagger.pTags?.let { notify(it.map { it.toPTag() }) } - val replyToSet = - if (forkedFromNote != null) { - (listOfNotNull(forkedFromNote) + (tagger.eTags ?: emptyList())).ifEmpty { null } - } else { - tagger.eTags + hashtags(findHashtags(tagger.message)) + references(findURLs(tagger.message)) + quotes(findNostrUris(tagger.message)) + + geoHash?.let { geohash(it) } + localZapRaiserAmount?.let { zapraiser(it) } + zapReceiver?.let { zapSplits(it) } + contentWarningReason?.let { contentWarning(it) } + + emojis(emojis) + imetas(usedAttachments) } - val repositoryAddress = originalNoteEvent.repository() + val broadcast = tagger.directMentionsNotes + (tagger.eTags ?: emptyList()) - account?.sendGitReply( - message = tagger.message, - replyTo = replyToSet, - mentions = tagger.pTags, - repository = repositoryAddress, - zapReceiver = zapReceiver, - wantsToMarkAsSensitive = wantsToMarkAsSensitive, - zapRaiserAmount = localZapRaiserAmount, - replyingTo = replyId, - root = rootId, - directMentions = tagger.directMentions, - forkedFrom = forkedFromNote?.event as? Event, - relayList = relayList, - geohash = geoHash, - imetas = usedAttachments, - emojis = emojis, - draftTag = localDraft, - ) + account?.signAndSend(localDraft, template, relayList, broadcast) } else if (originalNote?.event is TorrentCommentEvent) { - val originalNoteEvent = originalNote?.event as TorrentCommentEvent - // adds markers - val rootId = - originalNoteEvent.torrent() // if it has a marker as root - ?: originalNote - ?.replyTo - ?.firstOrNull { it.event != null && it.replyTo?.isEmpty() == true } - ?.idHex // if it has loaded events with zero replies in the reply list - ?: originalNote?.replyTo?.firstOrNull()?.idHex // old rules, first item is root. - ?: originalNote?.idHex + val replyToEvent = originalNote?.event as TorrentCommentEvent - if (rootId != null) { - // There must be a torrent ID - val replyId = originalNote?.idHex + val rootETag = replyToEvent.torrent() + val rootNote = rootETag?.eventId?.let { LocalCache.getNoteIfExists(it) } + val rootNoteEvent = rootNote?.event + // only uses the root node if the event is loaded. + val root = + if (rootNoteEvent != null) { + rootNote // refreshes author and relay hint to what we have. + } else { + rootETag?.let { LocalCache.getOrCreateNote(it) } // keeps what came in. + ?: originalNote?.replyTo?.firstOrNull { it.event != null && it.replyTo?.isEmpty() == true } // if it has loaded events with zero replies in the reply list + ?: originalNote?.replyTo?.firstOrNull() // old rules, first item is root. + ?: originalNote + } + + if (root != null) { val replyToSet = if (forkedFromNote != null) { (listOfNotNull(forkedFromNote) + (tagger.eTags ?: emptyList())).ifEmpty { null } @@ -778,29 +975,37 @@ open class NewPostViewModel : ViewModel() { tagger.eTags } - account?.sendTorrentComment( - message = tagger.message, - replyTo = replyToSet, - mentions = tagger.pTags, - zapReceiver = zapReceiver, - wantsToMarkAsSensitive = wantsToMarkAsSensitive, - zapRaiserAmount = localZapRaiserAmount, - replyingTo = replyId, - root = rootId, - directMentions = tagger.directMentions, - forkedFrom = forkedFromNote?.event as? Event, - relayList = relayList, - geohash = geoHash, - imetas = usedAttachments, - emojis = emojis, - draftTag = localDraft, - ) + val sortedAndMarked = + eTags?.map { it.toETag() }?.positionalMarkedTags( + root = root.toETag(), + replyingTo = replyingTo?.toETag(), + forkedFrom = forkedFromNote?.toETag(), + ) + + val template = + TorrentCommentEvent.build(tagger.message) { + sortedAndMarked?.let { eTags(sortedAndMarked) } + + pTags(tagger.directMentionsUsers.map { it.toPTag() }) + + hashtags(findHashtags(tagger.message)) + references(findURLs(tagger.message)) + quotes(findNostrUris(tagger.message)) + + geoHash?.let { geohash(it) } + localZapRaiserAmount?.let { zapraiser(it) } + zapReceiver?.let { zapSplits(it) } + contentWarningReason?.let { contentWarning(it) } + + emojis(emojis) + imetas(usedAttachments) + } + + val broadcast = tagger.directMentionsNotes + (replyToSet ?: emptySet()) + + account?.sendTorrentComment(localDraft, template, broadcast, relayList) } } else if (originalNote?.event is TorrentEvent) { - val originalNoteEvent = originalNote?.event as TorrentEvent - // adds markers - val rootId = originalNoteEvent.id - val replyToSet = if (forkedFromNote != null) { (listOfNotNull(forkedFromNote) + (tagger.eTags ?: emptyList())).ifEmpty { null } @@ -808,76 +1013,106 @@ open class NewPostViewModel : ViewModel() { tagger.eTags } - account?.sendTorrentComment( - message = tagger.message, - replyTo = replyToSet, - mentions = tagger.pTags, - zapReceiver = zapReceiver, - wantsToMarkAsSensitive = wantsToMarkAsSensitive, - zapRaiserAmount = localZapRaiserAmount, - replyingTo = null, - root = rootId, - directMentions = tagger.directMentions, - forkedFrom = forkedFromNote?.event as? Event, - relayList = relayList, - geohash = geoHash, - imetas = usedAttachments, - emojis = emojis, - draftTag = localDraft, - ) + val sortedAndMarked = + eTags?.map { it.toETag() }?.positionalMarkedTags( + root = originalNote?.toETag(), + replyingTo = null, + forkedFrom = forkedFromNote?.toETag(), + ) + + val template = + TorrentCommentEvent.build(tagger.message) { + sortedAndMarked?.let { eTags(sortedAndMarked) } + + hashtags(findHashtags(tagger.message)) + references(findURLs(tagger.message)) + quotes(findNostrUris(tagger.message)) + + geoHash?.let { geohash(it) } + localZapRaiserAmount?.let { zapraiser(it) } + zapReceiver?.let { zapSplits(it) } + contentWarningReason?.let { contentWarning(it) } + + emojis(emojis) + imetas(usedAttachments) + } + + val broadcast = tagger.directMentionsNotes + (replyToSet ?: emptySet()) + + account?.sendTorrentComment(localDraft, template, broadcast, relayList) } else { if (wantsPoll) { - account?.sendPoll( - message = tagger.message, - replyTo = tagger.eTags, - mentions = tagger.pTags, - pollOptions = pollOptions, - valueMaximum = valueMaximum, - valueMinimum = valueMinimum, - consensusThreshold = consensusThreshold, - closedAt = closedAt, - zapReceiver = zapReceiver, - wantsToMarkAsSensitive = wantsToMarkAsSensitive, - zapRaiserAmount = localZapRaiserAmount, - relayList = relayList, - geohash = geoHash, - imetas = usedAttachments, - emojis = emojis, - draftTag = localDraft, - ) + val options = pollOptions.map { PollOptionTag(it.key, it.value) } + + if (options.isEmpty()) return@withContext + + val quotes = findNostrUris(tagger.message) + + val template = + PollNoteEvent.build(tagger.message, options) { + valueMinimum?.let { minAmount(it) } + valueMaximum?.let { maxAmount(it) } + closedAt?.let { closedAt(it) } + consensusThreshold?.let { consensusThreshold(it / 100.0) } + + pTags(tagger.directMentionsUsers.map { it.toPTag() }) + quotes(quotes) + hashtags(findHashtags(tagger.message)) + + geoHash?.let { geohash(it) } + zapRaiserAmount?.let { zapraiser(it) } + zapReceiver?.let { zapSplits(it) } + contentWarningReason?.let { contentWarning(it) } + + emojis(emojis) + imetas(usedAttachments) + } + + account?.signAndSend(localDraft, template, relayList, quotes) } else if (wantsProduct) { - account?.sendClassifieds( - title = title.text, - price = Price(price.text, "SATS", null), - condition = condition, - message = tagger.message, - replyTo = tagger.eTags, - mentions = tagger.pTags, - location = locationText.text, - category = category.text, - directMentions = tagger.directMentions, - zapReceiver = zapReceiver, - wantsToMarkAsSensitive = wantsToMarkAsSensitive, - zapRaiserAmount = localZapRaiserAmount, - relayList = relayList, - geohash = geoHash, - imetas = usedAttachments, - emojis = emojis, - draftTag = localDraft, - ) + val images = + urls.mapNotNull { + val removedParamsFromUrl = + if (it.contains("?")) { + it.split("?")[0].lowercase() + } else if (it.contains("#")) { + it.split("#")[0].lowercase() + } else { + it + } + + if (imageExtensions.any { removedParamsFromUrl.endsWith(it) }) { + it + } else { + null + } + } + + val quotes = findNostrUris(tagger.message) + + val template = + ClassifiedsEvent.build( + title.text, + PriceTag(price.text, "SATS", null), + tagger.message, + locationText.text.ifBlank { null }, + condition, + images, + ) { + hashtags(listOfNotNull(category.text.ifBlank { null }) + findHashtags(tagger.message)) + quotes(quotes) + + geoHash?.let { geohash(it) } + zapRaiserAmount?.let { zapraiser(it) } + zapReceiver?.let { zapSplits(it) } + contentWarningReason?.let { contentWarning(it) } + + emojis(emojis) + imetas(usedAttachments) + } + + account?.signAndSend(localDraft, template, relayList, quotes) } else { - // adds markers - val rootId = - (originalNote?.event as? TextNoteEvent)?.root() // if it has a marker as root - ?: originalNote - ?.replyTo - ?.firstOrNull { it.event != null && it.replyTo?.isEmpty() == true } - ?.idHex // if it has loaded events with zero replies in the reply list - ?: originalNote?.replyTo?.firstOrNull()?.idHex // old rules, first item is root. - ?: originalNote?.idHex - - val replyId = originalNote?.idHex - val replyToSet = if (forkedFromNote != null) { (listOfNotNull(forkedFromNote) + (tagger.eTags ?: emptyList())).ifEmpty { null } @@ -885,24 +1120,30 @@ open class NewPostViewModel : ViewModel() { tagger.eTags } - account?.sendPost( - message = tagger.message, - replyTo = replyToSet, - mentions = tagger.pTags, - tags = null, - zapReceiver = zapReceiver, - wantsToMarkAsSensitive = wantsToMarkAsSensitive, - zapRaiserAmount = localZapRaiserAmount, - replyingTo = replyId, - root = rootId, - directMentions = tagger.directMentions, - forkedFrom = forkedFromNote?.event as? Event, - relayList = relayList, - geohash = geoHash, - imetas = usedAttachments, - emojis = emojis, - draftTag = localDraft, - ) + val template = + TextNoteEvent.build( + note = tagger.message, + replyingTo = originalNote?.toEventHint(), + forkingFrom = forkedFromNote?.toEventHint(), + ) { + tagger.pTags?.let { notify(it.map { it.toPTag() }) } + + hashtags(findHashtags(tagger.message)) + references(findURLs(tagger.message)) + quotes(findNostrUris(tagger.message)) + + geoHash?.let { geohash(it) } + localZapRaiserAmount?.let { zapraiser(it) } + zapReceiver?.let { zapSplits(it) } + contentWarningReason?.let { contentWarning(it) } + + emojis(emojis) + imetas(usedAttachments) + } + + val broadcast = tagger.directMentionsNotes + (replyToSet ?: emptySet()) + + account?.signAndSend(localDraft, template, relayList, broadcast) } } } @@ -910,16 +1151,16 @@ open class NewPostViewModel : ViewModel() { fun findEmoji( message: String, myEmojiSet: List?, - ): List { + ): List { if (myEmojiSet == null) return emptyList() return CustomEmoji.findAllEmojiCodes(message).mapNotNull { possibleEmoji -> - myEmojiSet.firstOrNull { it.code == possibleEmoji }?.let { EmojiUrl(it.code, it.url.url) } + myEmojiSet.firstOrNull { it.code == possibleEmoji }?.let { EmojiUrlTag(it.code, it.url.url) } } } fun upload( alt: String?, - sensitiveContent: Boolean, + contentWarningReason: String?, mediaQuality: Int, isPrivate: Boolean = false, server: ServerName, @@ -937,7 +1178,7 @@ open class NewPostViewModel : ViewModel() { myMultiOrchestrator.upload( viewModelScope, alt, - sensitiveContent, + contentWarningReason, MediaCompressor.intToCompressorQuality(mediaQuality), server, myAccount, @@ -947,7 +1188,7 @@ open class NewPostViewModel : ViewModel() { if (results.allGood) { results.successful.forEach { if (it.result is UploadOrchestrator.OrchestratorResult.NIP95Result) { - account?.createNip95(it.result.bytes, headerInfo = it.result.fileHeader, alt, sensitiveContent) { nip95 -> + account?.createNip95(it.result.bytes, headerInfo = it.result.fileHeader, alt, contentWarningReason) { nip95 -> nip95attachments = nip95attachments + nip95 val note = nip95.let { it1 -> account?.consumeNip95(it1.first, it1.second) } @@ -971,9 +1212,9 @@ open class NewPostViewModel : ViewModel() { ?.let { blurhash(it.blurhash) } it.result.magnet?.let { magnet(it) } it.result.uploadedHash?.let { originalHash(it) } + alt?.let { alt(it) } - // TODO: Support Reasons on images - if (sensitiveContent) sensitiveContent("") + contentWarningReason?.let { sensitiveContent(contentWarningReason) } }.build() iMetaAttachments = iMetaAttachments.filter { it.url != iMeta.url } + iMeta @@ -1021,7 +1262,7 @@ open class NewPostViewModel : ViewModel() { zapRaiserAmount = null wantsProduct = false - condition = ClassifiedsEvent.CONDITION.USED_LIKE_NEW + condition = ConditionTag.CONDITION.USED_LIKE_NEW locationText = TextFieldValue("") title = TextFieldValue("") category = TextFieldValue("") @@ -1333,47 +1574,19 @@ open class NewPostViewModel : ViewModel() { } fun updateMinZapAmountForPoll(textMin: String) { - if (textMin.isNotEmpty()) { - try { - val int = textMin.toInt() - if (int < 1) { - valueMinimum = null - } else { - valueMinimum = int - } - } catch (e: Exception) { - if (e is CancellationException) throw e - } - } else { - valueMinimum = null - } - + valueMinimum = textMin.toLongOrNull()?.takeIf { it > 0 } checkMinMax() saveDraft() } fun updateMaxZapAmountForPoll(textMax: String) { - if (textMax.isNotEmpty()) { - try { - val int = textMax.toInt() - if (int < 1) { - valueMaximum = null - } else { - valueMaximum = int - } - } catch (e: Exception) { - if (e is CancellationException) throw e - } - } else { - valueMaximum = null - } - + valueMaximum = textMax.toLongOrNull()?.takeIf { it > 0 } checkMinMax() saveDraft() } fun checkMinMax() { - if ((valueMinimum ?: 0) > (valueMaximum ?: Int.MAX_VALUE)) { + if ((valueMinimum ?: 0) > (valueMaximum ?: Long.MAX_VALUE)) { isValidvalueMinimum.value = false isValidvalueMaximum.value = false } else { @@ -1456,7 +1669,7 @@ open class NewPostViewModel : ViewModel() { saveDraft() } - fun updateCondition(newCondition: ClassifiedsEvent.CONDITION) { + fun updateCondition(newCondition: ConditionTag.CONDITION) { condition = newCondition saveDraft() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/AddDMRelayListDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/AddDMRelayListDialog.kt index 9ab6e2b1e..e1f480662 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/AddDMRelayListDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/AddDMRelayListDialog.kt @@ -53,7 +53,7 @@ import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.imageModifier -import com.vitorpamplona.quartz.nip01Core.relays.RelayStat +import com.vitorpamplona.quartz.nip01Core.relay.RelayStat @OptIn(ExperimentalMaterial3Api::class) @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/AddSearchRelayListDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/AddSearchRelayListDialog.kt index 0263895f6..feff3bacc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/AddSearchRelayListDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/AddSearchRelayListDialog.kt @@ -53,7 +53,7 @@ import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.imageModifier -import com.vitorpamplona.quartz.nip01Core.relays.RelayStat +import com.vitorpamplona.quartz.nip01Core.relay.RelayStat @OptIn(ExperimentalMaterial3Api::class) @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/AllRelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/AllRelayListView.kt index da22b4b76..24549b416 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/AllRelayListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/AllRelayListView.kt @@ -61,7 +61,7 @@ import com.vitorpamplona.amethyst.ui.theme.RowColSpacing import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.grayText import com.vitorpamplona.ammolite.relays.Constants -import com.vitorpamplona.quartz.nip01Core.relays.RelayStat +import com.vitorpamplona.quartz.nip01Core.relay.RelayStat @Composable fun AllRelayListView( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/BasicRelaySetupInfo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/BasicRelaySetupInfo.kt index 11fcdbde4..f7dacd37a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/BasicRelaySetupInfo.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/BasicRelaySetupInfo.kt @@ -24,7 +24,7 @@ import androidx.compose.runtime.Immutable import com.vitorpamplona.ammolite.relays.FeedType import com.vitorpamplona.ammolite.relays.RelayBriefInfoCache import com.vitorpamplona.quartz.nip01Core.HexKey -import com.vitorpamplona.quartz.nip01Core.relays.RelayStat +import com.vitorpamplona.quartz.nip01Core.relay.RelayStat @Immutable data class BasicRelaySetupInfo( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/Kind3RelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/Kind3RelayListView.kt index 85ddb78cf..0f850e99f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/Kind3RelayListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/Kind3RelayListView.kt @@ -94,7 +94,7 @@ import com.vitorpamplona.ammolite.relays.Constants import com.vitorpamplona.ammolite.relays.Constants.activeTypesGlobalChats import com.vitorpamplona.ammolite.relays.FeedType import com.vitorpamplona.ammolite.relays.RelayBriefInfoCache -import com.vitorpamplona.quartz.nip01Core.relays.RelayStat +import com.vitorpamplona.quartz.nip01Core.relay.RelayStat import com.vitorpamplona.quartz.nip65RelayList.RelayUrlFormatter import kotlinx.coroutines.launch diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/Kind3RelaySetupInfoProposalRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/Kind3RelaySetupInfoProposalRow.kt index 46f940903..c0e5233bb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/Kind3RelaySetupInfoProposalRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/Kind3RelaySetupInfoProposalRow.kt @@ -63,7 +63,7 @@ import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.ui.theme.allGoodColor import com.vitorpamplona.amethyst.ui.theme.largeRelayIconModifier import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES -import com.vitorpamplona.quartz.nip01Core.relays.RelayStat +import com.vitorpamplona.quartz.nip01Core.relay.RelayStat @OptIn(ExperimentalLayoutApi::class) @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/RelayUrlEditField.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/RelayUrlEditField.kt index 39b17364d..3b33cba4c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/RelayUrlEditField.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/RelayUrlEditField.kt @@ -40,7 +40,7 @@ import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.ui.theme.Size10dp import com.vitorpamplona.amethyst.ui.theme.placeholderText -import com.vitorpamplona.quartz.nip01Core.relays.RelayStat +import com.vitorpamplona.quartz.nip01Core.relay.RelayStat import com.vitorpamplona.quartz.nip65RelayList.RelayUrlFormatter @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt index 42ef9d7bc..11eb81bd4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt @@ -77,7 +77,7 @@ import com.vitorpamplona.quartz.nip19Bech32.entities.NPub import com.vitorpamplona.quartz.nip19Bech32.entities.NRelay import com.vitorpamplona.quartz.nip19Bech32.entities.NSec import com.vitorpamplona.quartz.nip19Bech32.toNIP19 -import com.vitorpamplona.quartz.nip28PublicChat.ChannelCreateEvent +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip30CustomEmoji.CustomEmoji import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableMap diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/VideoView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/VideoView.kt index 423ea160f..0fbc34067 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/VideoView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/VideoView.kt @@ -119,8 +119,8 @@ import com.vitorpamplona.amethyst.ui.theme.Size75dp import com.vitorpamplona.amethyst.ui.theme.VolumeBottomIconSize import com.vitorpamplona.amethyst.ui.theme.imageModifier import com.vitorpamplona.amethyst.ui.theme.videoGalleryModifier -import com.vitorpamplona.quartz.nip94FileMetadata.Dimension -import kotlinx.collections.immutable.ImmutableList +import com.vitorpamplona.quartz.experimental.audio.header.tags.WaveformTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -208,10 +208,10 @@ fun VideoView( roundedCorner: Boolean, gallery: Boolean = false, contentScale: ContentScale, - waveform: ImmutableList? = null, + waveform: WaveformTag? = null, artworkUri: String? = null, authorName: String? = null, - dimensions: Dimension? = null, + dimensions: DimensionTag? = null, blurhash: String? = null, nostrUriCallback: String? = null, onDialog: ((Boolean) -> Unit)? = null, @@ -239,10 +239,10 @@ fun VideoView( thumb: VideoThumb? = null, borderModifier: Modifier, contentScale: ContentScale, - waveform: ImmutableList? = null, + waveform: WaveformTag? = null, artworkUri: String? = null, authorName: String? = null, - dimensions: Dimension? = null, + dimensions: DimensionTag? = null, blurhash: String? = null, nostrUriCallback: String? = null, onDialog: ((Boolean) -> Unit)? = null, @@ -355,7 +355,7 @@ fun VideoViewInner( showControls: Boolean = true, contentScale: ContentScale, borderModifier: Modifier, - waveform: ImmutableList? = null, + waveform: WaveformTag? = null, artworkUri: String? = null, authorName: String? = null, nostrUriCallback: String? = null, @@ -732,7 +732,7 @@ private fun RenderVideoPlayer( showControls: Boolean = true, contentScale: ContentScale, nostrUriCallback: String?, - waveform: ImmutableList? = null, + waveform: WaveformTag? = null, keepPlaying: MutableState, automaticallyStartPlayback: State, activeOnScreen: MutableState, @@ -863,7 +863,7 @@ private fun pollCurrentDuration(controller: MediaController) = @Composable fun Waveform( - waveform: ImmutableList, + waveform: WaveformTag, controller: MediaController, modifier: Modifier, ) { @@ -897,13 +897,13 @@ fun Waveform( @Composable fun DrawWaveform( - waveform: ImmutableList, + waveform: WaveformTag, waveformProgress: MutableFloatState, modifier: Modifier, ) { AudioWaveformReadOnly( modifier = modifier.padding(start = 10.dp, end = 10.dp), - amplitudes = waveform, + amplitudes = waveform.wave, progress = waveformProgress.floatValue, progressBrush = Brush.infiniteLinearGradient( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt index 5f3c32e11..0cd971f28 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt @@ -97,7 +97,7 @@ import com.vitorpamplona.quartz.CryptoUtils import com.vitorpamplona.quartz.nip01Core.toHexKey import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent -import com.vitorpamplona.quartz.nip94FileMetadata.Dimension +import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.Dispatchers @@ -514,7 +514,7 @@ fun ShowHash(content: MediaUrlContent) { verifiedHash?.let { HashVerificationSymbol(it) } } -fun aspectRatio(dim: Dimension?): Float? { +fun aspectRatio(dim: DimensionTag?): Float? { if (dim == null) return null return dim.width.toFloat() / dim.height.toFloat() @@ -641,7 +641,7 @@ fun ShareImageAction( videoUri: String?, postNostrUri: String?, blurhash: String?, - dim: Dimension?, + dim: DimensionTag?, hash: String?, mimeType: String?, onDismiss: () -> Unit, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/markdown/MarkdownMediaRenderer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/markdown/MarkdownMediaRenderer.kt index e786fed2c..2f31da4e8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/markdown/MarkdownMediaRenderer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/markdown/MarkdownMediaRenderer.kt @@ -48,8 +48,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.LoadedBechLink import com.vitorpamplona.amethyst.ui.theme.Font17SP import com.vitorpamplona.amethyst.ui.theme.Size17Modifier -import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList -import com.vitorpamplona.quartz.nip02FollowList.ImmutableListOfLists import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress import com.vitorpamplona.quartz.nip19Bech32.entities.NEmbed import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent @@ -57,11 +55,12 @@ import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile import com.vitorpamplona.quartz.nip19Bech32.entities.NPub import com.vitorpamplona.quartz.nip19Bech32.entities.NRelay import com.vitorpamplona.quartz.nip19Bech32.entities.NSec +import com.vitorpamplona.quartz.nip92IMeta.IMetaTag import kotlinx.coroutines.runBlocking class MarkdownMediaRenderer( val startOfText: String, - val tags: ImmutableListOfLists?, + val imetaByUrl: Map, val canPreview: Boolean, val quotesLeft: Int, val backgroundColor: MutableState, @@ -94,7 +93,7 @@ class MarkdownMediaRenderer( val content = parser.createMediaContent( fullUrl = uri, - eventTags = tags ?: EmptyTagList, + eventTags = imetaByUrl, description = title?.ifEmpty { null } ?: startOfText, ) ?: MediaUrlImage(url = uri, description = title?.ifEmpty { null } ?: startOfText) @@ -116,7 +115,7 @@ class MarkdownMediaRenderer( uri: String, richTextStringBuilder: RichTextString.Builder, ) { - val content = parser.createMediaContent(uri, eventTags = tags ?: EmptyTagList, startOfText, callbackUri) + val content = parser.createMediaContent(uri, imetaByUrl, startOfText, callbackUri) if (canPreview) { if (content != null) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/markdown/RenderContentAsMarkdown.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/markdown/RenderContentAsMarkdown.kt index d7ce17ee5..6c5104216 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/markdown/RenderContentAsMarkdown.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/markdown/RenderContentAsMarkdown.kt @@ -49,10 +49,11 @@ import com.vitorpamplona.amethyst.ui.theme.MarkdownTextStyle import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow import com.vitorpamplona.amethyst.ui.theme.markdownStyle import com.vitorpamplona.amethyst.ui.uriToRoute -import com.vitorpamplona.quartz.nip01Core.MetadataEvent +import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList import com.vitorpamplona.quartz.nip02FollowList.ImmutableListOfLists import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip92IMeta.imetasByUrl import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext @@ -93,7 +94,7 @@ fun RenderContentAsMarkdown( remember(content) { MarkdownMediaRenderer( startOfText = content.take(100), - tags = tags, + imetaByUrl = tags?.lists?.imetasByUrl() ?: emptyMap(), canPreview = canPreview, quotesLeft = quotesLeft, backgroundColor = backgroundColor, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomFeedFilter.kt index 6293aeb4e..b1aa0119e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomFeedFilter.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.dal import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.quartz.nip17Dm.ChatroomKey +import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey class ChatroomFeedFilter( val withUser: ChatroomKey, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomListKnownFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomListKnownFeedFilter.kt index 8a7073c22..58c139aae 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomListKnownFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomListKnownFeedFilter.kt @@ -25,9 +25,9 @@ import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.actions.relays.updated import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEventIds -import com.vitorpamplona.quartz.nip17Dm.ChatroomKey -import com.vitorpamplona.quartz.nip17Dm.ChatroomKeyable -import com.vitorpamplona.quartz.nip28PublicChat.ChannelMessageEvent +import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey +import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable +import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent class ChatroomListKnownFeedFilter( val account: Account, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomListNewFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomListNewFeedFilter.kt index 1d55278a0..4ebefbe73 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomListNewFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomListNewFeedFilter.kt @@ -24,8 +24,8 @@ import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.actions.relays.updated import com.vitorpamplona.quartz.nip04Dm.PrivateDmEvent -import com.vitorpamplona.quartz.nip17Dm.ChatroomKey -import com.vitorpamplona.quartz.nip17Dm.ChatroomKeyable +import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey +import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable class ChatroomListNewFeedFilter( val account: Account, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/CommunityFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/CommunityFeedFilter.kt index 39d42849d..6c8e6561d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/CommunityFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/CommunityFeedFilter.kt @@ -26,7 +26,7 @@ import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.tags.addressables.isTaggedAddressableNote -import com.vitorpamplona.quartz.nip72ModCommunities.CommunityPostApprovalEvent +import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent class CommunityFeedFilter( val note: AddressableNote, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverChatFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverChatFeedFilter.kt index 0a26b7b32..f9d25420b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverChatFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverChatFeedFilter.kt @@ -24,8 +24,8 @@ import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.PublicChatChannel -import com.vitorpamplona.quartz.nip28PublicChat.ChannelCreateEvent -import com.vitorpamplona.quartz.nip28PublicChat.IsInPublicChatChannel +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent +import com.vitorpamplona.quartz.nip28PublicChat.base.IsInPublicChatChannel import com.vitorpamplona.quartz.nip51Lists.MuteListEvent import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent @@ -85,7 +85,7 @@ open class DiscoverChatFeedFilter( null } } else if (noteEvent is IsInPublicChatChannel) { - val channel = noteEvent.channel()?.let { LocalCache.checkGetOrCreateNote(it) } + val channel = noteEvent.channelId()?.let { LocalCache.checkGetOrCreateNote(it) } val channelEvent = channel?.event if (channel != null && diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverCommunityFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverCommunityFeedFilter.kt index 6693745b3..e5584738e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverCommunityFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverCommunityFeedFilter.kt @@ -27,8 +27,8 @@ import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip19Bech32.parseAtagUnckecked import com.vitorpamplona.quartz.nip51Lists.MuteListEvent import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent -import com.vitorpamplona.quartz.nip72ModCommunities.CommunityDefinitionEvent -import com.vitorpamplona.quartz.nip72ModCommunities.CommunityPostApprovalEvent +import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent +import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent open class DiscoverCommunityFeedFilter( val account: Account, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverLiveFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverLiveFeedFilter.kt index c31137858..85c56cba2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverLiveFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverLiveFeedFilter.kt @@ -26,10 +26,8 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.ParticipantListBuilder import com.vitorpamplona.quartz.nip51Lists.MuteListEvent import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent -import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesEvent -import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesEvent.Companion.STATUS_ENDED -import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesEvent.Companion.STATUS_LIVE -import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesEvent.Companion.STATUS_PLANNED +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.StatusTag open class DiscoverLiveFeedFilter( val account: Account, @@ -93,9 +91,9 @@ open class DiscoverLiveFeedFilter( fun convertStatusToOrder(status: String?): Int = when (status) { - STATUS_LIVE -> 2 - STATUS_PLANNED -> 1 - STATUS_ENDED -> 0 + StatusTag.STATUS.LIVE.code -> 2 + StatusTag.STATUS.PLANNED.code -> 1 + StatusTag.STATUS.ENDED.code -> 0 else -> 0 } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverNIP89FeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverNIP89FeedFilter.kt index 19b13c880..da10b3858 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverNIP89FeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverNIP89FeedFilter.kt @@ -25,7 +25,7 @@ import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.quartz.nip51Lists.MuteListEvent import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent -import com.vitorpamplona.quartz.nip89AppHandlers.AppDefinitionEvent +import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent import com.vitorpamplona.quartz.utils.TimeUtils open class DiscoverNIP89FeedFilter( @@ -76,7 +76,7 @@ open class DiscoverNIP89FeedFilter( val filterParams = buildFilterParams(account) return noteEvent.appMetaData()?.subscription != true && filterParams.match(noteEvent) && - noteEvent.includeKind("5300") && + noteEvent.includeKind(5300) && noteEvent.createdAt > TimeUtils.now() - lastAnnounced // && params.match(noteEvent) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/FilterByListParams.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/FilterByListParams.kt index b93515027..49e3e5715 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/FilterByListParams.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/FilterByListParams.kt @@ -31,7 +31,7 @@ import com.vitorpamplona.quartz.nip01Core.tags.hashtags.isTaggedHashes import com.vitorpamplona.quartz.nip22Comments.CommentEvent import com.vitorpamplona.quartz.nip51Lists.MuteListEvent import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent -import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesEvent +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.utils.TimeUtils class FilterByListParams( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/GeoHashFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/GeoHashFeedFilter.kt index 92ad7b3c7..c72513d8e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/GeoHashFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/GeoHashFeedFilter.kt @@ -23,13 +23,13 @@ package com.vitorpamplona.amethyst.ui.dal import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.quartz.experimental.audio.AudioHeaderEvent +import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent import com.vitorpamplona.quartz.nip01Core.tags.geohash.isTaggedGeoHash import com.vitorpamplona.quartz.nip04Dm.PrivateDmEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent -import com.vitorpamplona.quartz.nip28PublicChat.ChannelMessageEvent +import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent class GeoHashFeedFilter( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/HashtagFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/HashtagFeedFilter.kt index 2626e695d..d8db70276 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/HashtagFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/HashtagFeedFilter.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.ui.dal import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.quartz.experimental.audio.AudioHeaderEvent +import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent import com.vitorpamplona.quartz.nip01Core.tags.hashtags.isTaggedHash import com.vitorpamplona.quartz.nip04Dm.PrivateDmEvent @@ -31,7 +31,7 @@ import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent -import com.vitorpamplona.quartz.nip28PublicChat.ChannelMessageEvent +import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent class HashtagFeedFilter( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeConversationsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeConversationsFeedFilter.kt index f4678e056..e057bc091 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeConversationsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeConversationsFeedFilter.kt @@ -27,10 +27,10 @@ import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent -import com.vitorpamplona.quartz.nip28PublicChat.ChannelMessageEvent +import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent import com.vitorpamplona.quartz.nip51Lists.MuteListEvent import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent -import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesChatMessageEvent +import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent class HomeConversationsFeedFilter( val account: Account, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeNewThreadFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeNewThreadFeedFilter.kt index 87661230a..f1c079d0b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeNewThreadFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeNewThreadFeedFilter.kt @@ -23,8 +23,8 @@ package com.vitorpamplona.amethyst.ui.dal import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.quartz.experimental.audio.AudioHeaderEvent -import com.vitorpamplona.quartz.experimental.audio.AudioTrackEvent +import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent +import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt index 4a41b1e7f..3333f9716 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt @@ -23,23 +23,25 @@ package com.vitorpamplona.amethyst.ui.dal import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.quartz.experimental.forks.forkFromVersion +import com.vitorpamplona.quartz.experimental.forks.isForkFromAddressWithPubkey import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser -import com.vitorpamplona.quartz.nip10Notes.BaseTextNoteEvent +import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent -import com.vitorpamplona.quartz.nip28PublicChat.ChannelCreateEvent -import com.vitorpamplona.quartz.nip28PublicChat.ChannelMetadataEvent -import com.vitorpamplona.quartz.nip34Git.GitIssueEvent -import com.vitorpamplona.quartz.nip34Git.GitPatchEvent +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent +import com.vitorpamplona.quartz.nip34Git.issue.GitIssueEvent +import com.vitorpamplona.quartz.nip34Git.patch.GitPatchEvent import com.vitorpamplona.quartz.nip51Lists.MuteListEvent import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent import com.vitorpamplona.quartz.nip58Badges.BadgeDefinitionEvent import com.vitorpamplona.quartz.nip58Badges.BadgeProfilesEvent -import com.vitorpamplona.quartz.nip59Giftwrap.GiftWrapEvent +import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryRequestEvent import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryResponseEvent @@ -138,13 +140,13 @@ class NotificationFeedFilter( return true } - if (event is BaseTextNoteEvent) { + if (event is BaseThreadedEvent) { if (note.replyTo?.any { it.author?.pubkeyHex == authorHex } == true) return true val isAuthoredPostCited = event.findCitations().any { LocalCache.getNoteIfExists(it)?.author?.pubkeyHex == authorHex } val isAuthorDirectlyCited = event.citedUsers().contains(authorHex) val isAuthorOfAFork = - event.isForkFromAddressWithPubkey(authorHex) || (event.forkFromVersion()?.let { LocalCache.getNoteIfExists(it)?.author?.pubkeyHex == authorHex } == true) + event.isForkFromAddressWithPubkey(authorHex) || (event.forkFromVersion()?.let { LocalCache.getNoteIfExists(it.eventId)?.author?.pubkeyHex == authorHex } == true) return isAuthoredPostCited || isAuthorDirectlyCited || isAuthorOfAFork } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileAppRecommendationsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileAppRecommendationsFeedFilter.kt index 986457950..a6b515f5b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileAppRecommendationsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileAppRecommendationsFeedFilter.kt @@ -23,7 +23,8 @@ package com.vitorpamplona.amethyst.ui.dal import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.quartz.nip89AppHandlers.AppRecommendationEvent +import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip89AppHandlers.recommendation.AppRecommendationEvent class UserProfileAppRecommendationsFeedFilter( val user: User, @@ -47,7 +48,7 @@ class UserProfileAppRecommendationsFeedFilter( val noteEvent = it.event if (noteEvent is AppRecommendationEvent) { if (noteEvent.pubKey == user.pubkeyHex) { - return noteEvent.recommendations().map { LocalCache.getOrCreateAddressableNote(it) } + return noteEvent.recommendations().map { LocalCache.getOrCreateAddressableNote(ATag(it.address)) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileConversationsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileConversationsFeedFilter.kt index 18d10f1ce..66ca7fd2f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileConversationsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileConversationsFeedFilter.kt @@ -27,9 +27,9 @@ import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent -import com.vitorpamplona.quartz.nip28PublicChat.ChannelMessageEvent +import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent -import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesChatMessageEvent +import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent class UserProfileConversationsFeedFilter( val user: User, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileNewThreadFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileNewThreadFeedFilter.kt index 19c6e1e62..6e5676064 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileNewThreadFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileNewThreadFeedFilter.kt @@ -25,8 +25,8 @@ import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.quartz.experimental.audio.AudioHeaderEvent -import com.vitorpamplona.quartz.experimental.audio.AudioTrackEvent +import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent +import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/VideoFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/VideoFeedFilter.kt index 1c73d589a..565b2ba64 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/VideoFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/VideoFeedFilter.kt @@ -26,7 +26,7 @@ import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.SUPPORTED_VIDEO_FEED_MIME_TYPES_SET -import com.vitorpamplona.quartz.experimental.nip95.FileStorageHeaderEvent +import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip51Lists.MuteListEvent import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/RouteMaker.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/RouteMaker.kt index e64e4c701..f922b7cad 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/RouteMaker.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/RouteMaker.kt @@ -27,15 +27,15 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip17Dm.ChatroomKey -import com.vitorpamplona.quartz.nip17Dm.ChatroomKeyable -import com.vitorpamplona.quartz.nip28PublicChat.ChannelCreateEvent -import com.vitorpamplona.quartz.nip28PublicChat.IsInPublicChatChannel +import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey +import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent +import com.vitorpamplona.quartz.nip28PublicChat.base.IsInPublicChatChannel import com.vitorpamplona.quartz.nip37Drafts.DraftEvent -import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesChatMessageEvent -import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesEvent -import com.vitorpamplona.quartz.nip72ModCommunities.CommunityDefinitionEvent -import com.vitorpamplona.quartz.nip89AppHandlers.AppDefinitionEvent +import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent +import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent +import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent import kotlinx.collections.immutable.persistentSetOf import java.net.URLEncoder @@ -56,11 +56,11 @@ fun routeFor( val innerEvent = noteEvent.preCachedDraft(loggedIn.pubkeyHex) if (innerEvent is IsInPublicChatChannel) { - innerEvent.channel()?.let { + innerEvent.channelId()?.let { return "Channel/$it" } } else if (innerEvent is LiveActivitiesEvent) { - innerEvent.address().toTag().let { + innerEvent.aTag().toTag().let { return "Channel/${URLEncoder.encode(it, "utf-8")}" } } else if (innerEvent is LiveActivitiesChatMessageEvent) { @@ -72,20 +72,20 @@ fun routeFor( loggedIn.createChatroom(room) return "Room/${room.hashCode()}" } else if (innerEvent is AddressableEvent) { - return "Note/${URLEncoder.encode(noteEvent.address().toTag(), "utf-8")}" + return "Note/${URLEncoder.encode(noteEvent.aTag().toTag(), "utf-8")}" } else { return "Note/${URLEncoder.encode(noteEvent.id, "utf-8")}" } } else if (noteEvent is AppDefinitionEvent) { return "ContentDiscovery/${noteEvent.id}" } else if (noteEvent is IsInPublicChatChannel) { - noteEvent.channel()?.let { + noteEvent.channelId()?.let { return "Channel/$it" } } else if (noteEvent is ChannelCreateEvent) { return "Channel/${noteEvent.id}" } else if (noteEvent is LiveActivitiesEvent) { - noteEvent.address().toTag().let { + noteEvent.aTag().toTag().let { return "Channel/${URLEncoder.encode(it, "utf-8")}" } } else if (noteEvent is LiveActivitiesChatMessageEvent) { @@ -97,9 +97,9 @@ fun routeFor( loggedIn.createChatroom(room) return "Room/${room.hashCode()}" } else if (noteEvent is CommunityDefinitionEvent) { - return "Community/${URLEncoder.encode(noteEvent.address().toTag(), "utf-8")}" + return "Community/${URLEncoder.encode(noteEvent.aTag().toTag(), "utf-8")}" } else if (noteEvent is AddressableEvent) { - return "Note/${URLEncoder.encode(noteEvent.address().toTag(), "utf-8")}" + return "Note/${URLEncoder.encode(noteEvent.aTag().toTag(), "utf-8")}" } else { return "Note/${URLEncoder.encode(noteEvent.id, "utf-8")}" } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt index 5074e8fd5..2ffe22d51 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt @@ -102,16 +102,15 @@ import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.bitcoinColor import com.vitorpamplona.amethyst.ui.theme.grayText import com.vitorpamplona.amethyst.ui.theme.nip05 -import com.vitorpamplona.quartz.experimental.audio.Participant -import com.vitorpamplona.quartz.nip28PublicChat.ChannelCreateEvent -import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesEvent -import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesEvent.Companion.STATUS_ENDED -import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesEvent.Companion.STATUS_LIVE -import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesEvent.Companion.STATUS_PLANNED -import com.vitorpamplona.quartz.nip72ModCommunities.CommunityDefinitionEvent -import com.vitorpamplona.quartz.nip89AppHandlers.AppDefinitionEvent +import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.ParticipantTag +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.StatusTag +import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent +import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent -import com.vitorpamplona.quartz.nip99Classifieds.Price +import com.vitorpamplona.quartz.nip99Classifieds.tags.PriceTag import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -295,7 +294,7 @@ private fun RenderNoteRow( data class ClassifiedsThumb( val image: String?, val title: String?, - val price: Price?, + val price: PriceTag?, ) @Composable @@ -339,7 +338,7 @@ fun RenderClassifiedsThumbPreview() { ClassifiedsThumb( image = null, title = "Like New", - price = Price("800000", "SATS", null), + price = PriceTag("800000", "SATS", null), ), note = Note("hex"), ) @@ -416,7 +415,7 @@ data class LiveActivityCard( val media: String?, val subject: String?, val content: String?, - val participants: ImmutableList, + val participants: ImmutableList, val status: String?, val starts: Long?, ) @@ -485,7 +484,7 @@ fun RenderLiveActivityThumb( Box(Modifier.padding(10.dp)) { CrossfadeIfEnabled(targetState = card.status, label = "RenderLiveActivityThumb", accountViewModel = accountViewModel) { when (it) { - STATUS_LIVE -> { + StatusTag.STATUS.LIVE.code -> { val url = card.media if (url.isNullOrBlank()) { LiveFlag() @@ -499,10 +498,10 @@ fun RenderLiveActivityThumb( } } } - STATUS_ENDED -> { + StatusTag.STATUS.ENDED.code -> { EndedFlag() } - STATUS_PLANNED -> { + StatusTag.STATUS.PLANNED.code -> { ScheduledFlag(card.starts) } else -> { @@ -544,7 +543,7 @@ data class CommunityCard( val name: String, val description: String?, val cover: String?, - val moderators: ImmutableList, + val moderators: ImmutableList, ) @Immutable @@ -574,16 +573,16 @@ fun RenderCommunitiesThumb( CommunityCard( name = noteEvent?.dTag() ?: "", description = noteEvent?.description(), - cover = noteEvent?.image()?.ifBlank { null }, - moderators = noteEvent?.moderators()?.toImmutableList() ?: persistentListOf(), + cover = noteEvent?.image()?.imageUrl, + moderators = noteEvent?.moderatorKeys()?.toImmutableList() ?: persistentListOf(), ) }.distinctUntilChanged() .observeAsState( CommunityCard( name = noteEvent.dTag(), description = noteEvent.description(), - cover = noteEvent.image()?.ifBlank { null }, - moderators = noteEvent.moderators().toImmutableList(), + cover = noteEvent.image()?.imageUrl, + moderators = noteEvent.moderatorKeys().toImmutableList(), ), ) @@ -655,7 +654,7 @@ fun RenderCommunitiesThumb( @Composable fun LoadModerators( - moderators: ImmutableList, + moderators: ImmutableList, baseNote: Note, accountViewModel: AccountViewModel, content: @Composable (ImmutableList) -> Unit, @@ -670,8 +669,8 @@ fun LoadModerators( launch(Dispatchers.IO) { val hosts = moderators.mapNotNull { part -> - if (part.key != baseNote.author?.pubkeyHex) { - LocalCache.checkGetOrCreateUser(part.key) + if (part != baseNote.author?.pubkeyHex) { + LocalCache.checkGetOrCreateUser(part) } else { null } @@ -706,7 +705,7 @@ fun LoadModerators( @Composable private fun LoadParticipants( - participants: ImmutableList, + participants: ImmutableList, baseNote: Note, accountViewModel: AccountViewModel, inner: @Composable (ImmutableList) -> Unit, @@ -721,8 +720,8 @@ private fun LoadParticipants( launch(Dispatchers.IO) { val hosts = participants.mapNotNull { part -> - if (part.key != baseNote.author?.pubkeyHex) { - LocalCache.checkGetOrCreateUser(part.key) + if (part.pubKey != baseNote.author?.pubkeyHex) { + LocalCache.checkGetOrCreateUser(part.pubKey) } else { null } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt index 4dff1bd04..04d59482b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt @@ -42,6 +42,7 @@ import com.vitorpamplona.amethyst.ui.navigation.routeFor import com.vitorpamplona.amethyst.ui.note.elements.NoteDropDownMenu import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.MessageSetCard +import com.vitorpamplona.amethyst.ui.theme.StdStartPadding import kotlinx.coroutines.launch @OptIn(ExperimentalFoundationApi::class) @@ -53,7 +54,7 @@ fun MessageSetCompose( accountViewModel: AccountViewModel, nav: INav, ) { - val baseNote = remember { messageSetCard.note } + val baseNote = messageSetCard.note val popupExpanded = remember { mutableStateOf(false) } val enablePopup = remember { { popupExpanded.value = true } } @@ -90,15 +91,9 @@ fun MessageSetCompose( Column(columnModifier) { Row(Modifier.fillMaxWidth()) { - Box( - modifier = remember { Modifier.width(55.dp).padding(top = 5.dp, end = 5.dp) }, - ) { - MessageIcon( - remember { Modifier.size(16.dp).align(Alignment.TopEnd) }, - ) - } + MessageIconBox() - Column(modifier = remember { Modifier.padding(start = 10.dp) }) { + Column(modifier = StdStartPadding) { NoteCompose( baseNote = baseNote, routeForLastRead = null, @@ -117,3 +112,10 @@ fun MessageSetCompose( } } } + +@Composable +fun MessageIconBox() { + Box(Modifier.width(55.dp).padding(top = 5.dp, end = 5.dp)) { + MessageIcon(Modifier.size(16.dp).align(Alignment.TopEnd)) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt index f244233b1..78f5b10e0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt @@ -72,7 +72,7 @@ import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.lessImportantLink import com.vitorpamplona.amethyst.ui.theme.nip05 import com.vitorpamplona.amethyst.ui.theme.placeholderText -import com.vitorpamplona.quartz.nip01Core.UserMetadata +import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.addressables.firstTaggedAddress import com.vitorpamplona.quartz.nip01Core.tags.events.ETag diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 4d5e08d34..49d88fbac 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -149,36 +149,36 @@ import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor import com.vitorpamplona.amethyst.ui.theme.normalWithTopMarginNoteModifier import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.replyModifier -import com.vitorpamplona.quartz.experimental.audio.AudioHeaderEvent -import com.vitorpamplona.quartz.experimental.audio.AudioTrackEvent -import com.vitorpamplona.quartz.experimental.bounties.getReward +import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent +import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent +import com.vitorpamplona.quartz.experimental.bounties.bountyBaseReward import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent +import com.vitorpamplona.quartz.experimental.forks.isAFork import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryBaseEvent import com.vitorpamplona.quartz.experimental.medical.FhirResourceEvent -import com.vitorpamplona.quartz.experimental.nip95.FileStorageHeaderEvent +import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent import com.vitorpamplona.quartz.nip01Core.tags.addressables.isTaggedAddressableKind import com.vitorpamplona.quartz.nip01Core.tags.geohash.getGeoHash import com.vitorpamplona.quartz.nip04Dm.PrivateDmEvent -import com.vitorpamplona.quartz.nip10Notes.BaseTextNoteEvent +import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent -import com.vitorpamplona.quartz.nip13Pow.pow import com.vitorpamplona.quartz.nip13Pow.strongPoWOrNull -import com.vitorpamplona.quartz.nip17Dm.ChatMessageEncryptedFileHeaderEvent -import com.vitorpamplona.quartz.nip17Dm.ChatMessageEvent -import com.vitorpamplona.quartz.nip17Dm.ChatMessageRelayListEvent +import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent +import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent +import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent -import com.vitorpamplona.quartz.nip28PublicChat.ChannelCreateEvent -import com.vitorpamplona.quartz.nip28PublicChat.ChannelMessageEvent -import com.vitorpamplona.quartz.nip28PublicChat.ChannelMetadataEvent -import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiPackEvent -import com.vitorpamplona.quartz.nip34Git.GitIssueEvent -import com.vitorpamplona.quartz.nip34Git.GitPatchEvent -import com.vitorpamplona.quartz.nip34Git.GitRepositoryEvent +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent +import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent +import com.vitorpamplona.quartz.nip30CustomEmoji.pack.EmojiPackEvent +import com.vitorpamplona.quartz.nip34Git.issue.GitIssueEvent +import com.vitorpamplona.quartz.nip34Git.patch.GitPatchEvent +import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent import com.vitorpamplona.quartz.nip37Drafts.DraftEvent @@ -186,8 +186,8 @@ import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent import com.vitorpamplona.quartz.nip51Lists.PinListEvent import com.vitorpamplona.quartz.nip51Lists.RelaySetEvent -import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesChatMessageEvent -import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesEvent +import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent import com.vitorpamplona.quartz.nip56Reports.ReportEvent import com.vitorpamplona.quartz.nip57Zaps.splits.hasZapSplitSetup @@ -198,10 +198,10 @@ import com.vitorpamplona.quartz.nip68Picture.PictureEvent import com.vitorpamplona.quartz.nip71Video.VideoEvent import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent -import com.vitorpamplona.quartz.nip72ModCommunities.CommunityDefinitionEvent -import com.vitorpamplona.quartz.nip72ModCommunities.CommunityPostApprovalEvent +import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent +import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent -import com.vitorpamplona.quartz.nip89AppHandlers.AppDefinitionEvent +import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryResponseEvent import com.vitorpamplona.quartz.nip90Dvms.NIP90StatusEvent import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent @@ -963,7 +963,7 @@ fun SecondUserInfoRow( verticalAlignment = CenterVertically, modifier = UserNameMaxRowHeight, ) { - if (noteEvent is BaseTextNoteEvent && noteEvent.isAFork()) { + if (noteEvent is BaseThreadedEvent && noteEvent.isAFork()) { ShowForkInformation(noteEvent, remember(noteEvent) { Modifier.weight(1f) }, accountViewModel, nav) } else { ObserveDisplayNip05Status(noteAuthor, remember(noteEvent) { Modifier.weight(1f) }, accountViewModel, nav) @@ -975,7 +975,7 @@ fun SecondUserInfoRow( DisplayLocation(geo, nav) } - val baseReward = remember(noteEvent) { noteEvent.getReward()?.let { Reward(it) } } + val baseReward = remember(noteEvent) { noteEvent.bountyBaseReward()?.let { Reward(it) } } if (baseReward != null) { Spacer(StdHorzSpacer) DisplayReward(baseReward, note, accountViewModel, nav) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt index 02dd65b5f..7aec26e14 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt @@ -95,8 +95,8 @@ import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.WarningColor import com.vitorpamplona.amethyst.ui.theme.isLight import com.vitorpamplona.amethyst.ui.theme.secondaryButtonBackground -import com.vitorpamplona.quartz.experimental.audio.AudioTrackEvent -import com.vitorpamplona.quartz.experimental.bounties.getReward +import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent +import com.vitorpamplona.quartz.experimental.bounties.bountyBaseReward import com.vitorpamplona.quartz.nip19Bech32.toNAddr import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent @@ -124,7 +124,7 @@ val njumpLink = { nip19BechAddress: String -> val externalLinkForNote = { note: Note -> if (note is AddressableNote) { - if (note.event?.getReward() != null) { + if (note.event?.bountyBaseReward() != null) { "https://nostrbounties.com/b/${note.address().toNAddr()}" } else if (note.event is PeopleListEvent) { "https://listr.lol/a/${note.address().toNAddr()}" diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt index 518f766ca..d919e88ae 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt @@ -97,7 +97,7 @@ import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList import com.vitorpamplona.quartz.nip02FollowList.ImmutableListOfLists import com.vitorpamplona.quartz.nip02FollowList.toImmutableListOfLists -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer +import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -124,7 +124,7 @@ fun PollNotePreview() { arrayOf("poll_option", "2", "OP3"), arrayOf("value_maximum", "2"), arrayOf("value_minimum", "2"), - AltTagSerializer.toTagArray("Poll event"), + arrayOf("alt", "Poll event"), ), ) @@ -196,7 +196,7 @@ fun PollNotePreview2() { arrayOf("poll_option", "1", "Pesquisa em ingles"), arrayOf("value_maximum", "2"), arrayOf("value_minimum", "2"), - AltTagSerializer.toTagArray("Poll event"), + AltTag.assemble("Poll event"), ), ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt index 942278a7a..f9476e789 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt @@ -28,11 +28,7 @@ import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.quartz.experimental.zapPolls.CLOSED_AT -import com.vitorpamplona.quartz.experimental.zapPolls.CONSENSUS_THRESHOLD import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent -import com.vitorpamplona.quartz.experimental.zapPolls.VALUE_MAXIMUM -import com.vitorpamplona.quartz.experimental.zapPolls.VALUE_MINIMUM import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent import com.vitorpamplona.quartz.utils.TimeUtils @@ -81,17 +77,12 @@ class PollNoteViewModel : ViewModel() { pollNote = note pollEvent = pollNote?.event as PollNoteEvent pollOptions = pollEvent?.pollOptions() - valueMaximum = pollEvent?.getTagLong(VALUE_MAXIMUM) - valueMinimum = pollEvent?.getTagLong(VALUE_MINIMUM) + valueMaximum = pollEvent?.maxAmount() + valueMinimum = pollEvent?.minAmount() valueMinimumBD = valueMinimum?.let { BigDecimal(it) } valueMaximumBD = valueMaximum?.let { BigDecimal(it) } - consensusThreshold = - pollEvent - ?.getTagLong(CONSENSUS_THRESHOLD) - ?.toFloat() - ?.div(100) - ?.toBigDecimal() - closedAt = pollEvent?.getTagLong(CLOSED_AT) + consensusThreshold = pollEvent?.consensusThreshold()?.toBigDecimal() + closedAt = pollEvent?.closedAt() totalZapped = BigDecimal.ZERO wasZappedByLoggedInAccount = false diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt index af5e11a53..7965c62c7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt @@ -147,7 +147,7 @@ import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.reactionBox import com.vitorpamplona.amethyst.ui.theme.ripple24dp import com.vitorpamplona.amethyst.ui.theme.selectedReactionBoxModifier -import com.vitorpamplona.quartz.nip10Notes.BaseTextNoteEvent +import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent import com.vitorpamplona.quartz.nip30CustomEmoji.CustomEmoji import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount import kotlinx.collections.immutable.ImmutableList @@ -579,7 +579,7 @@ private fun BoostWithDialog( val replyTo = remember(wantsToFork) { val forkEvent = wantsToFork?.event - if (forkEvent is BaseTextNoteEvent) { + if (forkEvent is BaseThreadedEvent) { val hex = forkEvent.replyingTo() wantsToFork?.replyTo?.filter { it.event?.id == hex }?.firstOrNull() } else { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ShowEmojiSuggestionList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ShowEmojiSuggestionList.kt index aae7ebd3b..a6e229c3f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ShowEmojiSuggestionList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ShowEmojiSuggestionList.kt @@ -54,7 +54,7 @@ import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.Size10dp import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedAddresses -import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiPackSelectionEvent +import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiPackSelectionEvent import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.Flow diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateReactionTypeDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateReactionTypeDialog.kt index 2aba7ad5c..f157feeee 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateReactionTypeDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateReactionTypeDialog.kt @@ -89,8 +89,8 @@ import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedAddresses import com.vitorpamplona.quartz.nip30CustomEmoji.CustomEmoji -import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiPackSelectionEvent -import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrl +import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag +import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiPackSelectionEvent import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -116,7 +116,7 @@ class UpdateReactionTypeViewModel : ViewModel() { nextChoice = TextFieldValue("") } - fun addChoice(customEmoji: EmojiUrl) { + fun addChoice(customEmoji: EmojiUrlTag) { reactionSet = reactionSet + (customEmoji.encode()) } @@ -337,7 +337,7 @@ private fun RenderReactionOption( private fun EmojiSelector( accountViewModel: AccountViewModel, nav: INav, - onClick: ((EmojiUrl) -> Unit)? = null, + onClick: ((EmojiUrlTag) -> Unit)? = null, ) { LoadAddressableNote( aTag = @@ -372,7 +372,7 @@ fun EmojiCollectionGallery( emojiCollections: ImmutableList, accountViewModel: AccountViewModel, nav: INav, - onClick: ((EmojiUrl) -> Unit)? = null, + onClick: ((EmojiUrlTag) -> Unit)? = null, ) { val color = MaterialTheme.colorScheme.background val bgColor = remember { mutableStateOf(color) } @@ -396,7 +396,7 @@ private fun WatchAndRenderNote( bgColor: MutableState, accountViewModel: AccountViewModel, nav: INav, - onClick: ((EmojiUrl) -> Unit)?, + onClick: ((EmojiUrlTag) -> Unit)?, ) { val scope = rememberCoroutineScope() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt index 0e3fbc5f0..2a597c8c4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt @@ -46,7 +46,7 @@ import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.LoadUser import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.quartz.nip17Dm.ChatroomKey +import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey @Composable fun NoteAuthorPicture( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/AddInboxRelayForDMCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/AddInboxRelayForDMCard.kt index 3f29bca70..4ea7dafb7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/AddInboxRelayForDMCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/AddInboxRelayForDMCard.kt @@ -52,7 +52,7 @@ import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.ui.theme.imageModifier import com.vitorpamplona.quartz.nip01Core.HexKey -import com.vitorpamplona.quartz.nip17Dm.ChatMessageRelayListEvent +import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent @Preview @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayCommunity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayCommunity.kt index e8879681e..f1c4df4ae 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayCommunity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayCommunity.kt @@ -35,7 +35,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.addressables.getTagOfAddressableKind -import com.vitorpamplona.quartz.nip72ModCommunities.CommunityDefinitionEvent +import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent @Composable fun DisplayFollowingCommunityInPost( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayReward.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayReward.kt index 46c434484..64765a863 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayReward.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayReward.kt @@ -177,24 +177,19 @@ class AddBountyAmountViewModel : ViewModel() { } fun sendPost() { - val newValue = nextAmount.text.trim().toLongOrNull() + val newValue = nextAmount.text.trim().toBigDecimalOrNull() if (newValue != null) { viewModelScope.launch { - account?.let { - it.sendPost( - message = newValue.toString(), - replyTo = listOfNotNull(bounty), - mentions = listOfNotNull(bounty?.author), - tags = listOf("bounty-added-reward"), - wantsToMarkAsSensitive = false, - replyingTo = null, - root = null, - directMentions = setOf(), - forkedFrom = null, - draftTag = null, - relayList = it.activeWriteRelays().toImmutableList(), - ) + account?.let { myAccount -> + bounty?.let { bountyInner -> + myAccount.sendAddBounty( + newValue, + bountyInner, + draftTag = null, + relayList = myAccount.activeWriteRelays().toImmutableList(), + ) + } } nextAmount = TextFieldValue("") diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ForkInfo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ForkInfo.kt index 4a6376a24..54af94aca 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ForkInfo.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ForkInfo.kt @@ -45,11 +45,13 @@ import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.Font14SP import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.nip05 -import com.vitorpamplona.quartz.nip10Notes.BaseTextNoteEvent +import com.vitorpamplona.quartz.experimental.forks.forkFromAddress +import com.vitorpamplona.quartz.experimental.forks.forkFromVersion +import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent @Composable fun ShowForkInformation( - noteEvent: BaseTextNoteEvent, + noteEvent: BaseThreadedEvent, modifier: Modifier, accountViewModel: AccountViewModel, nav: INav, @@ -66,7 +68,7 @@ fun ShowForkInformation( } } } else if (forkedEvent != null) { - LoadNote(forkedEvent, accountViewModel = accountViewModel) { event -> + LoadNote(forkedEvent.eventId, accountViewModel = accountViewModel) { event -> if (event != null) { ForkInformationRowLightColor(event, modifier, accountViewModel, nav) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AppDefinition.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AppDefinition.kt index 907ff9612..3ee743da6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AppDefinition.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AppDefinition.kt @@ -72,8 +72,8 @@ import com.vitorpamplona.amethyst.ui.theme.Size35dp import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList import com.vitorpamplona.quartz.nip02FollowList.toImmutableListOfLists -import com.vitorpamplona.quartz.nip89AppHandlers.AppDefinitionEvent -import com.vitorpamplona.quartz.nip89AppHandlers.AppMetadata +import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent +import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppMetadata import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AudioTrack.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AudioTrack.kt index 704dd48a2..3535caff2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AudioTrack.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AudioTrack.kt @@ -51,15 +51,12 @@ import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture import com.vitorpamplona.amethyst.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.theme.placeholderText -import com.vitorpamplona.quartz.experimental.audio.AudioHeaderEvent -import com.vitorpamplona.quartz.experimental.audio.AudioTrackEvent -import com.vitorpamplona.quartz.experimental.audio.Participant +import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent +import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent +import com.vitorpamplona.quartz.experimental.audio.track.tags.ParticipantTag import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hasHashtags import com.vitorpamplona.quartz.nip02FollowList.toImmutableListOfLists import com.vitorpamplona.quartz.nip14Subject.subject -import kotlinx.collections.immutable.toImmutableList -import java.util.Locale @Composable fun RenderAudioTrack( @@ -84,10 +81,9 @@ fun AudioTrackHeader( val media = remember { noteEvent.media() } val cover = remember { noteEvent.cover() } val subject = remember { noteEvent.subject() } - val content = remember { noteEvent.content } val participants = remember { noteEvent.participants() } - var participantUsers by remember { mutableStateOf>>(emptyList()) } + var participantUsers by remember { mutableStateOf>>(emptyList()) } LaunchedEffect(key1 = participants) { accountViewModel.loadParticipants(participants) { participantUsers = it } @@ -129,13 +125,6 @@ fun AudioTrackHeader( Spacer(Modifier.width(5.dp)) UsernameDisplay(it.second, Modifier.weight(1f), accountViewModel = accountViewModel) Spacer(Modifier.width(5.dp)) - it.first.role?.let { - Text( - text = it.capitalize(Locale.ROOT), - color = MaterialTheme.colorScheme.placeholderText, - maxLines = 1, - ) - } } } @@ -193,7 +182,7 @@ fun AudioHeader( nav: INav, ) { val media = remember { noteEvent.stream() ?: noteEvent.download() } - val waveform = remember { noteEvent.wavefrom()?.toImmutableList()?.ifEmpty { null } } + val waveform = remember { noteEvent.wavefrom() } val content = remember { noteEvent.content.ifBlank { null } } val defaultBackground = MaterialTheme.colorScheme.background diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Badge.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Badge.kt index ae2b1dd4a..366bd7946 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Badge.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Badge.kt @@ -155,7 +155,7 @@ fun RenderBadgeAward( Text(text = stringRes(R.string.award_granted_to)) - LaunchedEffect(key1 = note) { accountViewModel.loadUsers(noteEvent.awardees()) { awardees = it } } + LaunchedEffect(key1 = note) { accountViewModel.loadUsers(noteEvent.awardeeIds()) { awardees = it } } FlowRow(modifier = Modifier.padding(top = 5.dp)) { awardees.take(100).forEach { user -> diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChannelMessage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChannelMessage.kt index 0f2a5eff6..b88b973bb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChannelMessage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChannelMessage.kt @@ -36,7 +36,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.ChannelHeader import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.replyModifier -import com.vitorpamplona.quartz.nip28PublicChat.ChannelMessageEvent +import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent @Composable fun RenderChannelMessage( @@ -53,7 +53,7 @@ fun RenderChannelMessage( val showChannelInfo = remember(noteEvent) { if (noteEvent is ChannelMessageEvent) { - noteEvent.channel() + noteEvent.channelId() } else { null } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChatMessage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChatMessage.kt index 9e3c1705e..054b6b891 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChatMessage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChatMessage.kt @@ -39,7 +39,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.ChatroomHeader import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.replyModifier -import com.vitorpamplona.quartz.nip17Dm.ChatroomKeyable +import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable @Composable fun RenderChatMessage( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChatMessageEncryptedFile.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChatMessageEncryptedFile.kt index c60a99b21..b62136669 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChatMessageEncryptedFile.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChatMessageEncryptedFile.kt @@ -55,10 +55,11 @@ import com.vitorpamplona.amethyst.ui.theme.HalfVertPadding import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.replyModifier import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList -import com.vitorpamplona.quartz.nip17Dm.AESGCM -import com.vitorpamplona.quartz.nip17Dm.ChatMessageEncryptedFileHeaderEvent -import com.vitorpamplona.quartz.nip17Dm.ChatroomKeyable +import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable +import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent +import com.vitorpamplona.quartz.nip17Dm.files.encryption.AESGCM import com.vitorpamplona.quartz.nip21UriScheme.toNostrUri +import com.vitorpamplona.quartz.nip31Alts.alt import kotlinx.collections.immutable.persistentListOf @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt index 528d39188..9b27beb67 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt @@ -75,11 +75,11 @@ import com.vitorpamplona.amethyst.ui.theme.Size35dp import com.vitorpamplona.amethyst.ui.theme.Size5dp import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.innerPostModifier -import com.vitorpamplona.quartz.experimental.audio.Participant import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hasHashtags import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList import com.vitorpamplona.quartz.nip14Subject.subject -import com.vitorpamplona.quartz.nip72ModCommunities.CommunityDefinitionEvent +import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent +import com.vitorpamplona.quartz.nip72ModCommunities.definition.tags.ModeratorTag import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import java.util.Locale @@ -196,7 +196,7 @@ fun LongCommunityHeader( var participantUsers by remember(baseNote) { - mutableStateOf>>( + mutableStateOf>>( persistentListOf(), ) } @@ -265,7 +265,7 @@ fun ShortCommunityHeader( noteEvent.image()?.let { RobohashFallbackAsyncImage( robot = baseNote.idHex, - model = it, + model = it.imageUrl, contentDescription = stringRes(R.string.profile_image), contentScale = ContentScale.Crop, modifier = HeaderPictureModifier, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Emoji.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Emoji.kt index 41cbe7dd6..239ae4656 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Emoji.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Emoji.kt @@ -58,9 +58,9 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.Size35Modifier import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.addressables.isTaggedAddressableNote -import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiPackEvent -import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiPackSelectionEvent -import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrl +import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag +import com.vitorpamplona.quartz.nip30CustomEmoji.pack.EmojiPackEvent +import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiPackSelectionEvent import com.vitorpamplona.quartz.nip30CustomEmoji.taggedEmojis @Composable @@ -69,7 +69,7 @@ public fun RenderEmojiPack( actionable: Boolean, backgroundColor: MutableState, accountViewModel: AccountViewModel, - onClick: ((EmojiUrl) -> Unit)? = null, + onClick: ((EmojiUrlTag) -> Unit)? = null, ) { val noteEvent by baseNote @@ -101,7 +101,7 @@ public fun RenderEmojiPack( actionable: Boolean, backgroundColor: MutableState, accountViewModel: AccountViewModel, - onClick: ((EmojiUrl) -> Unit)? = null, + onClick: ((EmojiUrlTag) -> Unit)? = null, ) { var expanded by remember { mutableStateOf(false) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileHeader.kt index 11410be70..cb69b0d89 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileHeader.kt @@ -33,6 +33,7 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.components.ZoomableContentView import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileStorage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileStorage.kt index b608b1903..dca60e57e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileStorage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileStorage.kt @@ -37,7 +37,8 @@ import com.vitorpamplona.amethyst.ui.components.LoadNote import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.components.ZoomableContentView import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.quartz.experimental.nip95.FileStorageHeaderEvent +import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent +import com.vitorpamplona.quartz.nip31Alts.alt import java.io.File @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Git.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Git.kt index fcab0a63f..03e3304ca 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Git.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Git.kt @@ -66,9 +66,9 @@ import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList import com.vitorpamplona.quartz.nip02FollowList.toImmutableListOfLists import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip14Subject.subject -import com.vitorpamplona.quartz.nip34Git.GitIssueEvent -import com.vitorpamplona.quartz.nip34Git.GitPatchEvent -import com.vitorpamplona.quartz.nip34Git.GitRepositoryEvent +import com.vitorpamplona.quartz.nip34Git.issue.GitIssueEvent +import com.vitorpamplona.quartz.nip34Git.patch.GitPatchEvent +import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent @Composable fun RenderGitPatchEvent( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Highlight.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Highlight.kt index eaa07279b..b0f0c9c96 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Highlight.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Highlight.kt @@ -57,7 +57,7 @@ import com.vitorpamplona.quartz.nip01Core.core.firstTagValueFor import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.events.ETag import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList -import com.vitorpamplona.quartz.nip10Notes.BaseTextNoteEvent +import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent import com.vitorpamplona.quartz.nip19Bech32.toNIP19 import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent import kotlinx.coroutines.Dispatchers @@ -79,7 +79,7 @@ fun RenderHighlight( DisplayHighlight( highlight = noteEvent.quote(), context = noteEvent.context(), - authorHex = noteEvent.author(), + authorHex = noteEvent.pubKey, url = noteEvent.inUrl(), postAddress = noteEvent.inPost(), postVersion = noteEvent.inPostVersion(), @@ -247,7 +247,7 @@ fun DisplayEntryForNote( RenderUserAsClickableText(author, null, nav) } - val noteEvent = noteState?.note?.event as? BaseTextNoteEvent ?: return + val noteEvent = noteState?.note?.event as? BaseThreadedEvent ?: return val description = remember(noteEvent) { noteEvent.tags.firstTagValueFor("title", "subject", "alt") } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/InteractiveStory.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/InteractiveStory.kt index d5e1a5beb..5237d3a03 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/InteractiveStory.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/InteractiveStory.kt @@ -72,7 +72,7 @@ fun RenderInteractiveStory( val currentScene = readingState?.currentScene() - if (currentScene != null && currentScene != rootEvent.address()) { + if (currentScene != null && currentScene != rootEvent.aTag()) { LoadAddressableNote(currentScene, accountViewModel) { currentSceneBaseNote -> val currentScene = currentSceneBaseNote?.live()?.metadata?.observeAsState() val currentSceneEvent = currentScene?.value?.note?.event as? InteractiveStoryBaseEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivity.kt index 98a9dfed5..6efc13e20 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivity.kt @@ -69,9 +69,10 @@ import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.ui.theme.imageModifier import com.vitorpamplona.amethyst.ui.theme.placeholderText -import com.vitorpamplona.quartz.experimental.audio.Participant import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesEvent +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.ParticipantTag +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.StatusTag import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -159,11 +160,11 @@ fun RenderLiveActivityEventInner( CrossfadeIfEnabled(targetState = status, label = "RenderLiveActivityEventInner", accountViewModel = accountViewModel) { when (it) { - LiveActivitiesEvent.STATUS_LIVE -> { + StatusTag.STATUS.LIVE.code -> { media?.let { CrossfadeCheckIfVideoIsOnline(it, accountViewModel) { LiveFlag() } } } - LiveActivitiesEvent.STATUS_PLANNED -> { + StatusTag.STATUS.PLANNED.code -> { ScheduledFlag(starts) } } @@ -171,7 +172,7 @@ fun RenderLiveActivityEventInner( } media?.let { media -> - if (status == LiveActivitiesEvent.STATUS_LIVE) { + if (status == StatusTag.STATUS.LIVE.code) { CheckIfVideoIsOnline(media, accountViewModel) { isOnline -> if (isOnline) { Row( @@ -206,7 +207,7 @@ fun RenderLiveActivityEventInner( } } } else { - if (status == LiveActivitiesEvent.STATUS_ENDED || (status == LiveActivitiesEvent.STATUS_PLANNED && (starts ?: 0) < TimeUtils.eightHoursAgo())) { + if (status == StatusTag.STATUS.ENDED.code || (status == StatusTag.STATUS.PLANNED.code && (starts ?: 0) < TimeUtils.eightHoursAgo())) { Box( contentAlignment = Alignment.Center, modifier = @@ -233,7 +234,7 @@ fun RenderLiveActivityEventInner( } var participantUsers by remember { - mutableStateOf>>( + mutableStateOf>>( persistentListOf(), ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivityChatMessage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivityChatMessage.kt index 91e3f9e0f..472d06f64 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivityChatMessage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivityChatMessage.kt @@ -36,7 +36,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.ChannelHeader import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.replyModifier -import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesChatMessageEvent +import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent @Composable fun RenderLiveActivityChatMessage( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PeopleList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PeopleList.kt index d78900ec2..0da9a10ac 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PeopleList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PeopleList.kt @@ -94,7 +94,7 @@ fun DisplayPeopleList( ) LaunchedEffect(Unit) { - accountViewModel.loadUsers(noteEvent.bookmarkedPeople()) { + accountViewModel.loadUsers(noteEvent.bookmarkedPeopleIds()) { members = it } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Poll.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Poll.kt index 555b2eae3..090ce4d94 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Poll.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Poll.kt @@ -47,7 +47,7 @@ import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hasHashtags import com.vitorpamplona.quartz.nip01Core.tags.people.hasAnyTaggedUser import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList import com.vitorpamplona.quartz.nip02FollowList.toImmutableListOfLists -import com.vitorpamplona.quartz.nip72ModCommunities.CommunityDefinitionEvent +import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent @Composable fun RenderPoll( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PrivateMessage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PrivateMessage.kt index da8310554..b217cce08 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PrivateMessage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PrivateMessage.kt @@ -52,7 +52,7 @@ import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hasHashtags import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList import com.vitorpamplona.quartz.nip02FollowList.toImmutableListOfLists import com.vitorpamplona.quartz.nip04Dm.PrivateDmEvent -import com.vitorpamplona.quartz.nip17Dm.ChatroomKeyable +import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable import com.vitorpamplona.quartz.nip19Bech32.toNpub @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt index 6cbc97a19..df646155b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt @@ -55,7 +55,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.ammolite.relays.RelayBriefInfoCache import com.vitorpamplona.quartz.nip01Core.core.firstTagValueFor -import com.vitorpamplona.quartz.nip17Dm.ChatMessageRelayListEvent +import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent import com.vitorpamplona.quartz.nip51Lists.RelaySetEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RenderPostApproval.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RenderPostApproval.kt index 05517f21e..b67dcf95e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RenderPostApproval.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RenderPostApproval.kt @@ -41,7 +41,7 @@ import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.replyModifier -import com.vitorpamplona.quartz.nip72ModCommunities.CommunityPostApprovalEvent +import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent @Composable fun RenderPostApproval( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt index 6157de2b6..434631753 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt @@ -49,10 +49,10 @@ import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hasHashtags import com.vitorpamplona.quartz.nip01Core.tags.people.hasAnyTaggedUser import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList import com.vitorpamplona.quartz.nip02FollowList.toImmutableListOfLists -import com.vitorpamplona.quartz.nip10Notes.BaseTextNoteEvent +import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip14Subject.subject -import com.vitorpamplona.quartz.nip72ModCommunities.CommunityDefinitionEvent +import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent @Composable fun RenderTextEvent( @@ -71,14 +71,14 @@ fun RenderTextEvent( val showReply by remember(note) { derivedStateOf { - noteEvent is BaseTextNoteEvent && !makeItShort && unPackReply && (note.replyTo != null || noteEvent.hasAnyTaggedUser()) + noteEvent is BaseThreadedEvent && !makeItShort && unPackReply && (note.replyTo != null || noteEvent.hasAnyTaggedUser()) } } if (showReply) { val replyingDirectlyTo = remember(note) { - if (noteEvent is BaseTextNoteEvent) { + if (noteEvent is BaseThreadedEvent) { val replyingTo = noteEvent.replyingToAddressOrEvent() if (replyingTo != null) { val newNote = accountViewModel.getNoteIfExists(replyingTo) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TextModification.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TextModification.kt index ebdbd62a7..cef42b915 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TextModification.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TextModification.kt @@ -84,7 +84,7 @@ fun RenderTextModificationEvent( val isAuthorTheLoggedUser = remember { val authorOfTheOriginalNote = - noteEvent.editedNote()?.let { accountViewModel.getNoteIfExists(it.eventId)?.author?.pubkeyHex ?: it.authorPubKeyHex } + noteEvent.editedNote()?.let { accountViewModel.getNoteIfExists(it.eventId)?.author?.pubkeyHex ?: it.author } mutableStateOf(accountViewModel.isLoggedUser(authorOfTheOriginalNote)) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Torrent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Torrent.kt index c2902ac3d..2fbcefca7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Torrent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Torrent.kt @@ -65,7 +65,7 @@ import com.vitorpamplona.amethyst.ui.theme.Size30dp import com.vitorpamplona.amethyst.ui.theme.Size5dp import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent -import com.vitorpamplona.quartz.nip35Torrents.TorrentFile +import com.vitorpamplona.quartz.nip35Torrents.tags.FileTag import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers @@ -177,7 +177,7 @@ fun RenderTorrent( @Composable fun DisplayFileList( - files: ImmutableList, + files: ImmutableList, name: String, description: String?, link: () -> String, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TorrentComment.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TorrentComment.kt index 28dcb1522..e7444f8bf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TorrentComment.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TorrentComment.kt @@ -179,7 +179,7 @@ fun RenderTorrentComment( torrentInfo?.let { TorrentHeader( - torrentHex = it, + torrentHex = it.eventId, modifier = MaterialTheme.colorScheme.replyModifier.padding(10.dp), accountViewModel = accountViewModel, nav = nav, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Video.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Video.kt index 6259245d5..882053d59 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Video.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Video.kt @@ -61,6 +61,7 @@ import com.vitorpamplona.amethyst.ui.theme.imageModifier import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hasHashtags import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList import com.vitorpamplona.quartz.nip02FollowList.toImmutableListOfLists +import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.nip71Video.VideoEvent @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VideoDisplay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VideoDisplay.kt index 57c773478..f7556eee3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VideoDisplay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VideoDisplay.kt @@ -33,6 +33,7 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.components.ZoomableContentView import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.nip71Video.VideoEvent @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt index 5e2412e03..ee0ba65c8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt @@ -37,14 +37,15 @@ import com.vitorpamplona.amethyst.ui.tor.TorSettings import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow import com.vitorpamplona.ammolite.relays.Constants import com.vitorpamplona.quartz.CryptoUtils -import com.vitorpamplona.quartz.nip01Core.KeyPair -import com.vitorpamplona.quartz.nip01Core.MetadataEvent +import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.hexToByteArray +import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync import com.vitorpamplona.quartz.nip01Core.toHexKey -import com.vitorpamplona.quartz.nip02FollowList.Contact import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent -import com.vitorpamplona.quartz.nip17Dm.ChatMessageRelayListEvent +import com.vitorpamplona.quartz.nip02FollowList.ContactTag +import com.vitorpamplona.quartz.nip02FollowList.ReadWrite +import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser import com.vitorpamplona.quartz.nip19Bech32.bech32.bechToBytes import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress @@ -282,11 +283,11 @@ class AccountStateViewModel : ViewModel() { backupUserMetadata = MetadataEvent.newUser(name, tempSigner), backupContactList = ContactListEvent.createFromScratch( - followUsers = listOf(Contact(keyPair.pubKey.toHexKey(), null)), + followUsers = listOf(ContactTag(keyPair.pubKey.toHexKey(), null, null)), followEvents = DefaultChannels.toList(), relayUse = Constants.defaultRelays.associate { - it.url to ContactListEvent.ReadWrite(it.read, it.write) + it.url to ReadWrite(it.read, it.write) }, signer = tempSigner, ), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt index 24de4b228..f63c4c79b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt @@ -57,7 +57,7 @@ import com.vitorpamplona.amethyst.ui.dal.UserProfileReportsFeedFilter import com.vitorpamplona.amethyst.ui.feeds.FeedContentState import com.vitorpamplona.amethyst.ui.feeds.FeedState import com.vitorpamplona.amethyst.ui.feeds.InvalidatableContent -import com.vitorpamplona.quartz.nip17Dm.ChatroomKey +import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FollowListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FollowListState.kt index a1d9026e6..c6bfd6c39 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FollowListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FollowListState.kt @@ -34,8 +34,8 @@ import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.quartz.experimental.audio.AudioHeaderEvent -import com.vitorpamplona.quartz.experimental.audio.AudioTrackEvent +import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent +import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent @@ -47,10 +47,10 @@ import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent import com.vitorpamplona.quartz.nip51Lists.MuteListEvent import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent import com.vitorpamplona.quartz.nip51Lists.PinListEvent -import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesChatMessageEvent -import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesEvent +import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent -import com.vitorpamplona.quartz.nip72ModCommunities.CommunityPostApprovalEvent +import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent import kotlinx.collections.immutable.persistentListOf diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index d97bb5edd..7d246191c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -72,21 +72,21 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.showAmountAxi import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.tor.TorSettings import com.vitorpamplona.ammolite.relays.BundledInsert -import com.vitorpamplona.quartz.experimental.audio.Participant import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryBaseEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryReadingStateEvent import com.vitorpamplona.quartz.nip01Core.HexKey -import com.vitorpamplona.quartz.nip01Core.KeyPair -import com.vitorpamplona.quartz.nip01Core.UserMetadata import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair +import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip01Core.tags.people.PubKeyReferenceTag import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser -import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUsers +import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUserIds import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation -import com.vitorpamplona.quartz.nip17Dm.ChatMessageRelayListEvent -import com.vitorpamplona.quartz.nip17Dm.ChatroomKey -import com.vitorpamplona.quartz.nip17Dm.ChatroomKeyable +import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey +import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable +import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser @@ -105,12 +105,12 @@ import com.vitorpamplona.quartz.nip56Reports.ReportEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount -import com.vitorpamplona.quartz.nip59Giftwrap.GiftWrapEvent -import com.vitorpamplona.quartz.nip59Giftwrap.SealedRumorEvent +import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent +import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.nip65RelayList.RelayUrlFormatter import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryResponseEvent -import com.vitorpamplona.quartz.nip94FileMetadata.Dimension +import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag import com.vitorpamplona.quartz.utils.TimeUtils import fr.acinq.secp256k1.Hex import kotlinx.collections.immutable.ImmutableList @@ -769,9 +769,9 @@ class AccountViewModel( fun addEmojiPack( usersEmojiList: Note, - emojiList: Note, + emojiPack: Note, ) { - viewModelScope.launch(Dispatchers.IO) { account.addEmojiPack(usersEmojiList, emojiList) } + viewModelScope.launch(Dispatchers.IO) { account.addEmojiPack(usersEmojiList, emojiPack) } } fun addMediaToGallery( @@ -779,7 +779,7 @@ class AccountViewModel( url: String, relay: String?, blurhash: String?, - dim: Dimension?, + dim: DimensionTag?, hash: String?, mimeType: String?, ) { @@ -1179,15 +1179,15 @@ class AccountViewModel( fun getChannelIfExists(hex: HexKey): Channel? = LocalCache.getChannelIfExists(hex) - fun loadParticipants( - participants: List, - onReady: (ImmutableList>) -> Unit, + fun loadParticipants( + participants: List, + onReady: (ImmutableList>) -> Unit, ) { viewModelScope.launch(Dispatchers.IO) { val participantUsers = participants .mapNotNull { part -> - checkGetOrCreateUser(part.key)?.let { + checkGetOrCreateUser(part.pubKey)?.let { Pair( part, it, @@ -1221,7 +1221,7 @@ class AccountViewModel( viewModelScope.launch(Dispatchers.Default) { account.decryptPeopleList(event) { privateTagList -> onReady( - (event.taggedUsers() + event.filterUsers(privateTagList)) + (event.taggedUserIds() + event.filterUsers(privateTagList)) .toSet() .mapNotNull { hex -> checkGetOrCreateUser(hex) } .sortedBy { account.isFollowing(it) } @@ -1539,7 +1539,7 @@ class AccountViewModel( ): Note { val note = if (innerEvent is AddressableEvent) { - AddressableNote(innerEvent.address()) + AddressableNote(innerEvent.aTag()) } else { Note(innerEvent.id) } @@ -1605,7 +1605,7 @@ class AccountViewModel( readingScene: InteractiveStoryBaseEvent, ) { viewModelScope.launch(Dispatchers.IO) { - val sceneNoteRelayHint = LocalCache.getOrCreateAddressableNote(readingScene.address()).relayHintUrl() + val sceneNoteRelayHint = LocalCache.getOrCreateAddressableNote(readingScene.aTag()).relayHintUrl() val readingState = getInteractiveStoryReadingState(root.addressTag()) val readingStateEvent = readingState.event as? InteractiveStoryReadingStateEvent @@ -1613,7 +1613,7 @@ class AccountViewModel( if (readingStateEvent != null) { account.updateInteractiveStoryReadingState(readingStateEvent, readingScene, sceneNoteRelayHint) } else { - val rootNoteRelayHint = LocalCache.getOrCreateAddressableNote(root.address()).relayHintUrl() + val rootNoteRelayHint = LocalCache.getOrCreateAddressableNote(root.aTag()).relayHintUrl() account.createInteractiveStoryReadingState(root, rootNoteRelayHint, readingScene, sceneNoteRelayHint) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoadRedirectScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoadRedirectScreen.kt index a3d440fba..f4c7d4bfb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoadRedirectScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoadRedirectScreen.kt @@ -45,14 +45,14 @@ import com.vitorpamplona.amethyst.ui.navigation.Route import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip17Dm.ChatroomKeyable -import com.vitorpamplona.quartz.nip28PublicChat.ChannelCreateEvent -import com.vitorpamplona.quartz.nip28PublicChat.ChannelMessageEvent -import com.vitorpamplona.quartz.nip28PublicChat.ChannelMetadataEvent -import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesChatMessageEvent -import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesEvent -import com.vitorpamplona.quartz.nip59Giftwrap.GiftWrapEvent -import com.vitorpamplona.quartz.nip59Giftwrap.SealedRumorEvent +import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent +import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent +import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent +import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent +import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -134,11 +134,11 @@ fun redirect( event is LiveActivitiesChatMessageEvent || event is LiveActivitiesEvent ) { - (event as? ChannelMessageEvent)?.channel() - ?: (event as? ChannelMetadataEvent)?.channel() + (event as? ChannelMessageEvent)?.channelId() + ?: (event as? ChannelMetadataEvent)?.channelId() ?: (event as? ChannelCreateEvent)?.id ?: (event as? LiveActivitiesChatMessageEvent)?.activity()?.toTag() - ?: (event as? LiveActivitiesEvent)?.address()?.toTag() + ?: (event as? LiveActivitiesEvent)?.aTag()?.toTag() } else { null } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NewPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NewPostScreen.kt index 9eac03605..bf4295c85 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NewPostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NewPostScreen.kt @@ -182,7 +182,7 @@ import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.replyModifier import com.vitorpamplona.amethyst.ui.theme.subtleBorder -import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent +import com.vitorpamplona.quartz.nip99Classifieds.tags.ConditionTag import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -521,7 +521,7 @@ fun NewPostScreen( it, accountViewModel.account.settings.defaultFileServer, onAdd = { alt, server, sensitiveContent, mediaQuality -> - postViewModel.upload(alt, sensitiveContent, mediaQuality, false, server, accountViewModel::toast, context) + postViewModel.upload(alt, if (sensitiveContent) "" else null, mediaQuality, false, server, accountViewModel::toast, context) if (server.type != ServerType.NIP95) { accountViewModel.account.settings.changeDefaultFileServer(server) } @@ -977,22 +977,22 @@ fun SellProduct(postViewModel: NewPostViewModel) { val conditionTypes = listOf( Triple( - ClassifiedsEvent.CONDITION.NEW, + ConditionTag.CONDITION.NEW, stringRes(id = R.string.classifieds_condition_new), stringRes(id = R.string.classifieds_condition_new_explainer), ), Triple( - ClassifiedsEvent.CONDITION.USED_LIKE_NEW, + ConditionTag.CONDITION.USED_LIKE_NEW, stringRes(id = R.string.classifieds_condition_like_new), stringRes(id = R.string.classifieds_condition_like_new_explainer), ), Triple( - ClassifiedsEvent.CONDITION.USED_GOOD, + ConditionTag.CONDITION.USED_GOOD, stringRes(id = R.string.classifieds_condition_good), stringRes(id = R.string.classifieds_condition_good_explainer), ), Triple( - ClassifiedsEvent.CONDITION.USED_FAIR, + ConditionTag.CONDITION.USED_FAIR, stringRes(id = R.string.classifieds_condition_fair), stringRes(id = R.string.classifieds_condition_fair_explainer), ), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chatrooms/ChannelScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chatrooms/ChannelScreen.kt index 995113294..a2d4e5ab6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chatrooms/ChannelScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chatrooms/ChannelScreen.kt @@ -165,14 +165,27 @@ import com.vitorpamplona.amethyst.ui.theme.ZeroPadding import com.vitorpamplona.amethyst.ui.theme.innerPostModifier import com.vitorpamplona.amethyst.ui.theme.liveStreamTag import com.vitorpamplona.amethyst.ui.theme.placeholderText -import com.vitorpamplona.quartz.experimental.audio.Participant +import com.vitorpamplona.quartz.nip01Core.EventHintBundle +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag import com.vitorpamplona.quartz.nip01Core.tags.events.isTaggedEvent import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hasHashtags +import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags +import com.vitorpamplona.quartz.nip01Core.tags.references.references import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList import com.vitorpamplona.quartz.nip02FollowList.toImmutableListOfLists +import com.vitorpamplona.quartz.nip10Notes.content.findHashtags +import com.vitorpamplona.quartz.nip10Notes.content.findNostrUris import com.vitorpamplona.quartz.nip10Notes.content.findURLs +import com.vitorpamplona.quartz.nip18Reposts.quotes.quotes import com.vitorpamplona.quartz.nip21UriScheme.toNostrUri -import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesEvent.Companion.STATUS_LIVE +import com.vitorpamplona.quartz.nip28PublicChat.base.notify +import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent +import com.vitorpamplona.quartz.nip30CustomEmoji.emojis +import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent +import com.vitorpamplona.quartz.nip53LiveActivities.chat.notify +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.ParticipantTag +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.StatusTag +import com.vitorpamplona.quartz.nip92IMeta.imetas import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -403,29 +416,93 @@ private suspend fun innerSendPost( val usedAttachments = newPostModel.iMetaAttachments.filter { it.url in urls.toSet() } val emojis = newPostModel.findEmoji(newPostModel.message.text, accountViewModel.account.myEmojis.value) + val channelRelays = channel.relays() + if (channel is PublicChatChannel) { - accountViewModel.account.sendChannelMessage( - message = tagger.message, - toChannel = channel.idHex, - replyTo = tagger.eTags, - mentions = tagger.pTags, - directMentions = tagger.directMentions, - wantsToMarkAsSensitive = false, - imetas = usedAttachments, - emojis = emojis, - draftTag = draftTag, - ) + val replyingToEvent = replyTo.value?.toEventHint() + val channelEvent = channel.event + + val template = + if (replyingToEvent != null) { + ChannelMessageEvent.reply(tagger.message, replyingToEvent) { + notify(replyingToEvent.toPTag()) + + hashtags(findHashtags(tagger.message)) + references(findURLs(tagger.message)) + quotes(findNostrUris(tagger.message)) + + emojis(emojis) + imetas(usedAttachments) + } + } else if (channelEvent != null) { + val hint = EventHintBundle(channelEvent, channelRelays.firstOrNull()) + ChannelMessageEvent.message(tagger.message, hint) { + hashtags(findHashtags(tagger.message)) + references(findURLs(tagger.message)) + quotes(findNostrUris(tagger.message)) + + emojis(emojis) + imetas(usedAttachments) + } + } else { + ChannelMessageEvent.message(tagger.message, ETag(channel.idHex, channelRelays.firstOrNull())) { + hashtags(findHashtags(tagger.message)) + references(findURLs(tagger.message)) + quotes(findNostrUris(tagger.message)) + + emojis(emojis) + imetas(usedAttachments) + } + } + + val broadcast = tagger.directMentionsNotes + (tagger.eTags ?: emptyList()) + + accountViewModel.account.signAndSendWithList(draftTag, template, channelRelays, broadcast) } else if (channel is LiveActivitiesChannel) { - accountViewModel.account.sendLiveMessage( - message = tagger.message, - toChannel = channel.address, - replyTo = tagger.eTags, - mentions = tagger.pTags, - wantsToMarkAsSensitive = false, - imetas = usedAttachments, - emojis = emojis, - draftTag = draftTag, - ) + val replyingToEvent = replyTo.value?.toEventHint() + val activity = channel.info + + val template = + if (replyingToEvent != null) { + LiveActivitiesChatMessageEvent.reply(tagger.message, replyingToEvent) { + notify(replyingToEvent.toPTag()) + + hashtags(findHashtags(tagger.message)) + references(findURLs(tagger.message)) + quotes(findNostrUris(tagger.message)) + + emojis(emojis) + imetas(usedAttachments) + } + } else if (activity != null) { + val hint = EventHintBundle(activity, channelRelays.firstOrNull() ?: replyingToEvent?.relay) + + LiveActivitiesChatMessageEvent.message(tagger.message, hint) { + hashtags(findHashtags(tagger.message)) + references(findURLs(tagger.message)) + quotes(findNostrUris(tagger.message)) + + emojis(emojis) + imetas(usedAttachments) + } + } else { + if (channel.address.relay == null) { + channel.address.relay = channelRelays.firstOrNull() ?: replyingToEvent?.relay + } + + LiveActivitiesChatMessageEvent.message(tagger.message, channel.address) { + hashtags(findHashtags(tagger.message)) + references(findURLs(tagger.message)) + quotes(findNostrUris(tagger.message)) + + emojis(emojis) + imetas(usedAttachments) + } + } + + val broadcast = tagger.directMentionsNotes + (tagger.eTags ?: emptyList()) + + accountViewModel.account.signAndSendWithList(draftTag, template, channelRelays, broadcast) } } @@ -533,7 +610,7 @@ fun EditFieldRow( channelScreenModel.selectImage(it) channelScreenModel.upload( alt = null, - sensitiveContent = false, + contentWarningReason = null, // Use MEDIUM quality mediaQuality = MediaCompressor.compressorQualityToInt(CompressorQuality.MEDIUM), server = accountViewModel.account.settings.defaultFileServer, @@ -988,21 +1065,20 @@ fun LongChannelHeader( } } - var participantUsers by - remember(baseChannel) { - mutableStateOf>>( + if (channel is LiveActivitiesChannel) { + var participantUsers by remember(baseChannel) { + mutableStateOf>>( persistentListOf(), ) } - if (channel is LiveActivitiesChannel) { LaunchedEffect(key1 = channelState) { launch(Dispatchers.IO) { val newParticipantUsers = channel.info ?.participants() ?.mapNotNull { part -> - LocalCache.checkGetOrCreateUser(part.key)?.let { Pair(part, it) } + LocalCache.checkGetOrCreateUser(part.pubKey)?.let { Pair(part, it) } }?.toImmutableList() if ( @@ -1135,7 +1211,7 @@ private fun LiveChannelActionOptions( accountViewModel: AccountViewModel, nav: INav, ) { - val isLive by remember(channel) { derivedStateOf { channel.info?.status() == STATUS_LIVE } } + val isLive by remember(channel) { derivedStateOf { channel.info?.status() == StatusTag.STATUS.LIVE.code } } val note = remember(channel.idHex) { LocalCache.getNoteIfExists(channel.idHex) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chatrooms/ChatFileUploadModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chatrooms/ChatFileUploadModel.kt index 78e7a1f67..344e39f11 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chatrooms/ChatFileUploadModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chatrooms/ChatFileUploadModel.kt @@ -31,6 +31,7 @@ import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.uploads.MediaCompressor import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator @@ -39,8 +40,11 @@ import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.quartz.nip17Dm.AESGCM -import com.vitorpamplona.quartz.nip17Dm.ChatroomKey +import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey +import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent +import com.vitorpamplona.quartz.nip17Dm.files.encryption.AESGCM +import com.vitorpamplona.quartz.nip31Alts.alt +import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -98,7 +102,7 @@ open class ChatFileUploadModel : ViewModel() { myMultiOrchestrator.uploadEncrypted( viewModelScope, caption, - sensitiveContent, + if (sensitiveContent) "" else null, MediaCompressor.intToCompressorQuality(mediaQualitySlider), cipher, mySelectedServer, @@ -109,24 +113,30 @@ open class ChatFileUploadModel : ViewModel() { if (results.allGood) { results.successful.forEach { state -> if (state.result is UploadOrchestrator.OrchestratorResult.ServerResult) { - account?.sendNIP17EncryptedFile( - url = state.result.url, - toUsers = myChatroom.users.toList(), - replyingTo = null, - contentType = state.result.mimeTypeBeforeEncryption, - algo = cipher.name(), - key = cipher.keyBytes, - nonce = cipher.nonce, - originalHash = state.result.hashBeforeEncryption, - hash = state.result.fileHeader.hash, - size = state.result.fileHeader.size, - dimensions = state.result.fileHeader.dim, - blurhash = - state.result.fileHeader.blurHash - ?.blurhash, - alt = caption, - sensitiveContent = sensitiveContent, - ) + val template = + ChatMessageEncryptedFileHeaderEvent.build( + url = state.result.url, + to = myChatroom.users.map { LocalCache.getOrCreateUser(it).toPTag() }, + cipher = cipher, + mimeType = state.result.mimeTypeBeforeEncryption, + originalHash = state.result.hashBeforeEncryption, + hash = state.result.fileHeader.hash, + size = state.result.fileHeader.size, + dimension = state.result.fileHeader.dim, + blurhash = + state.result.fileHeader.blurHash + ?.blurhash, + ) { + if (caption.isNotEmpty()) { + alt(caption) + } + + if (sensitiveContent) { + contentWarning("") + } + } + + account?.sendNIP17EncryptedFile(template) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chatrooms/ChatroomHeaderCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chatrooms/ChatroomHeaderCompose.kt index 7120cff01..7f8914674 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chatrooms/ChatroomHeaderCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chatrooms/ChatroomHeaderCompose.kt @@ -81,10 +81,10 @@ import com.vitorpamplona.amethyst.ui.theme.Size55dp import com.vitorpamplona.amethyst.ui.theme.grayText import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.quartz.nip01Core.HexKey -import com.vitorpamplona.quartz.nip17Dm.ChatroomKey -import com.vitorpamplona.quartz.nip17Dm.ChatroomKeyable -import com.vitorpamplona.quartz.nip28PublicChat.ChannelCreateEvent -import com.vitorpamplona.quartz.nip28PublicChat.ChannelMetadataEvent +import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey +import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent import com.vitorpamplona.quartz.nip37Drafts.DraftEvent @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chatrooms/ChatroomMessageCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chatrooms/ChatroomMessageCompose.kt index 6b441473e..30737d867 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chatrooms/ChatroomMessageCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chatrooms/ChatroomMessageCompose.kt @@ -105,11 +105,11 @@ import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList import com.vitorpamplona.quartz.nip02FollowList.ImmutableListOfLists import com.vitorpamplona.quartz.nip02FollowList.toImmutableListOfLists import com.vitorpamplona.quartz.nip04Dm.PrivateDmEvent -import com.vitorpamplona.quartz.nip17Dm.ChatMessageEncryptedFileHeaderEvent -import com.vitorpamplona.quartz.nip17Dm.ChatroomKeyable -import com.vitorpamplona.quartz.nip17Dm.NIP17Group -import com.vitorpamplona.quartz.nip28PublicChat.ChannelCreateEvent -import com.vitorpamplona.quartz.nip28PublicChat.ChannelMetadataEvent +import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable +import com.vitorpamplona.quartz.nip17Dm.base.NIP17Group +import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent import com.vitorpamplona.quartz.nip37Drafts.DraftEvent @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chatrooms/ChatroomScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chatrooms/ChatroomScreen.kt index e1f2019d9..e77750fca 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chatrooms/ChatroomScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chatrooms/ChatroomScreen.kt @@ -79,6 +79,7 @@ import androidx.lifecycle.distinctUntilChanged import androidx.lifecycle.map import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.NostrChatroomDataSource @@ -118,9 +119,18 @@ import com.vitorpamplona.amethyst.ui.theme.Size34dp import com.vitorpamplona.amethyst.ui.theme.StdPadding import com.vitorpamplona.amethyst.ui.theme.ZeroPadding import com.vitorpamplona.amethyst.ui.theme.placeholderText +import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags +import com.vitorpamplona.quartz.nip01Core.tags.references.references +import com.vitorpamplona.quartz.nip10Notes.content.findHashtags +import com.vitorpamplona.quartz.nip10Notes.content.findNostrUris import com.vitorpamplona.quartz.nip10Notes.content.findURLs -import com.vitorpamplona.quartz.nip17Dm.ChatroomKey -import com.vitorpamplona.quartz.nip17Dm.NIP17Group +import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey +import com.vitorpamplona.quartz.nip17Dm.base.NIP17Group +import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent +import com.vitorpamplona.quartz.nip17Dm.messages.changeSubject +import com.vitorpamplona.quartz.nip18Reposts.quotes.quotes +import com.vitorpamplona.quartz.nip30CustomEmoji.emojis +import com.vitorpamplona.quartz.nip92IMeta.imetas import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentSetOf import kotlinx.collections.immutable.toPersistentList @@ -519,24 +529,39 @@ private fun innerSendPost( val usedAttachments = newPostModel.iMetaAttachments.filter { it.url !in urls.toSet() } val emojis = newPostModel.findEmoji(newPostModel.message.text, accountViewModel.account.myEmojis.value) + val message = newPostModel.message.text + if (newPostModel.nip17 || room.users.size > 1 || replyTo.value?.event is NIP17Group) { - accountViewModel.account.sendNIP17PrivateMessage( - message = newPostModel.message.text, - toUsers = room.users.toList(), - replyingTo = replyTo.value, - mentions = null, - wantsToMarkAsSensitive = false, - imetas = usedAttachments, - emojis = emojis, - draftTag = dTag, - ) + val replyHint = replyTo.value?.toEventHint() + val template = + if (replyHint == null) { + ChatMessageEvent.build(message, room.users.map { LocalCache.getOrCreateUser(it).toPTag() }) { + hashtags(findHashtags(message)) + references(findURLs(message)) + quotes(findNostrUris(message)) + + emojis(emojis) + imetas(usedAttachments) + } + } else { + ChatMessageEvent.reply(message, replyHint) { + hashtags(findHashtags(message)) + references(findURLs(message)) + quotes(findNostrUris(message)) + + emojis(emojis) + imetas(usedAttachments) + } + } + + accountViewModel.account.sendNIP17PrivateMessage(template, dTag) } else { accountViewModel.account.sendPrivateMessage( message = newPostModel.message.text, - toUser = room.users.first(), + toUser = room.users.first().let { LocalCache.getOrCreateUser(it).toPTag() }, replyingTo = replyTo.value, mentions = null, - wantsToMarkAsSensitive = false, + contentWarningReason = null, imetas = usedAttachments, draftTag = dTag, ) @@ -862,14 +887,15 @@ fun NewSubjectView( PostButton( onPost = { scope.launch(Dispatchers.IO) { - accountViewModel.account.sendNIP17PrivateMessage( - message = message.value, - toUsers = room.users.toList(), - subject = groupName.value.ifBlank { null }, - replyingTo = null, - mentions = null, - wantsToMarkAsSensitive = false, - ) + val template = + ChatMessageEvent.build( + message.value, + room.users.map { LocalCache.getOrCreateUser(it).toPTag() }, + ) { + groupName.value.ifBlank { null }?.let { changeSubject(it) } + } + + accountViewModel.account.sendNIP17PrivateMessage(template) } onClose() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/DiscoverScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/DiscoverScreen.kt index 9ba6e2fa5..f21abbea7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/DiscoverScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/DiscoverScreen.kt @@ -78,10 +78,10 @@ import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.FeedPadding import com.vitorpamplona.amethyst.ui.theme.TabRowHeight -import com.vitorpamplona.quartz.nip28PublicChat.ChannelCreateEvent -import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesEvent -import com.vitorpamplona.quartz.nip72ModCommunities.CommunityDefinitionEvent -import com.vitorpamplona.quartz.nip89AppHandlers.AppDefinitionEvent +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent +import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent +import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt index c99bc0d74..d7dd79387 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt @@ -95,8 +95,8 @@ import com.vitorpamplona.amethyst.ui.theme.Size35dp import com.vitorpamplona.amethyst.ui.theme.Size75dp import com.vitorpamplona.quartz.lightning.LnInvoiceUtil import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceErrorResponse -import com.vitorpamplona.quartz.nip89AppHandlers.AppDefinitionEvent -import com.vitorpamplona.quartz.nip89AppHandlers.AppMetadata +import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent +import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppMetadata import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryResponseEvent import com.vitorpamplona.quartz.nip90Dvms.NIP90StatusEvent import kotlinx.collections.immutable.ImmutableList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedContentState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedContentState.kt index aa9ac23ac..0f9e94663 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedContentState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedContentState.kt @@ -39,12 +39,12 @@ import com.vitorpamplona.amethyst.ui.feeds.LoadedFeedState import com.vitorpamplona.ammolite.relays.BundledInsert import com.vitorpamplona.ammolite.relays.BundledUpdate import com.vitorpamplona.quartz.nip04Dm.PrivateDmEvent -import com.vitorpamplona.quartz.nip17Dm.NIP17Group +import com.vitorpamplona.quartz.nip17Dm.base.NIP17Group import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent -import com.vitorpamplona.quartz.nip28PublicChat.ChannelCreateEvent -import com.vitorpamplona.quartz.nip28PublicChat.ChannelMetadataEvent +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip58Badges.BadgeAwardEvent import kotlinx.collections.immutable.ImmutableList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationSummaryState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationSummaryState.kt index 150691d95..b6ea64b98 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationSummaryState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationSummaryState.kt @@ -36,7 +36,7 @@ import com.vitorpamplona.amethyst.ui.note.showCount import com.vitorpamplona.ammolite.relays.BundledInsert import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser -import com.vitorpamplona.quartz.nip10Notes.BaseTextNoteEvent +import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent @@ -126,7 +126,7 @@ class NotificationSummaryState( (zaps[netDate] ?: BigDecimal.ZERO) + (noteEvent.amount ?: BigDecimal.ZERO) takenIntoAccount.add(noteEvent.id) } - } else if (noteEvent is BaseTextNoteEvent) { + } else if (noteEvent is BaseThreadedEvent) { if (noteEvent.isTaggedUser(currentUser) && noteEvent.pubKey != currentUser) { val isCitation = noteEvent.findCitations().any { @@ -194,7 +194,7 @@ class NotificationSummaryState( takenIntoAccount.add(noteEvent.id) hasNewElements = true } - } else if (noteEvent is BaseTextNoteEvent) { + } else if (noteEvent is BaseThreadedEvent) { if (noteEvent.isTaggedUser(currentUser) && noteEvent.pubKey != currentUser) { val isCitation = noteEvent.findCitations().any { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/ProfileScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/ProfileScreen.kt index fb71eb80f..988eb7096 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/ProfileScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/ProfileScreen.kt @@ -191,7 +191,7 @@ import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceSuccessResponse import com.vitorpamplona.quartz.nip56Reports.ReportEvent import com.vitorpamplona.quartz.nip58Badges.BadgeDefinitionEvent import com.vitorpamplona.quartz.nip58Badges.BadgeProfilesEvent -import com.vitorpamplona.quartz.nip89AppHandlers.AppDefinitionEvent +import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/ShowQRDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/ShowQRDialog.kt index 989050479..7d360ad6f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/ShowQRDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/ShowQRDialog.kt @@ -65,7 +65,7 @@ import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.Font14SP import com.vitorpamplona.amethyst.ui.theme.Size10dp import com.vitorpamplona.amethyst.ui.theme.Size35dp -import com.vitorpamplona.quartz.nip01Core.UserMetadata +import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata @Preview @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt index d9cd9d07d..c067946cd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt @@ -163,32 +163,32 @@ import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.ui.theme.lessImportantLink import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.selectedNote -import com.vitorpamplona.quartz.experimental.audio.AudioHeaderEvent -import com.vitorpamplona.quartz.experimental.audio.AudioTrackEvent -import com.vitorpamplona.quartz.experimental.bounties.getReward +import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent +import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent +import com.vitorpamplona.quartz.experimental.bounties.bountyBaseReward import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent +import com.vitorpamplona.quartz.experimental.forks.forkFromAddress import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryBaseEvent import com.vitorpamplona.quartz.experimental.medical.FhirResourceEvent -import com.vitorpamplona.quartz.experimental.nip95.FileStorageHeaderEvent +import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.tags.addressables.isTaggedAddressableKind import com.vitorpamplona.quartz.nip01Core.tags.geohash.getGeoHash import com.vitorpamplona.quartz.nip04Dm.PrivateDmEvent -import com.vitorpamplona.quartz.nip13Pow.pow import com.vitorpamplona.quartz.nip13Pow.strongPoWOrNull -import com.vitorpamplona.quartz.nip17Dm.ChatMessageRelayListEvent +import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent -import com.vitorpamplona.quartz.nip28PublicChat.ChannelCreateEvent -import com.vitorpamplona.quartz.nip28PublicChat.ChannelMessageEvent -import com.vitorpamplona.quartz.nip28PublicChat.ChannelMetadataEvent -import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiPackEvent -import com.vitorpamplona.quartz.nip34Git.GitIssueEvent -import com.vitorpamplona.quartz.nip34Git.GitPatchEvent -import com.vitorpamplona.quartz.nip34Git.GitRepositoryEvent +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent +import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent +import com.vitorpamplona.quartz.nip30CustomEmoji.pack.EmojiPackEvent +import com.vitorpamplona.quartz.nip34Git.issue.GitIssueEvent +import com.vitorpamplona.quartz.nip34Git.patch.GitPatchEvent +import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent import com.vitorpamplona.quartz.nip37Drafts.DraftEvent @@ -196,17 +196,17 @@ import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent import com.vitorpamplona.quartz.nip51Lists.PinListEvent import com.vitorpamplona.quartz.nip51Lists.RelaySetEvent -import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesChatMessageEvent +import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent import com.vitorpamplona.quartz.nip57Zaps.splits.hasZapSplitSetup import com.vitorpamplona.quartz.nip58Badges.BadgeDefinitionEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.nip68Picture.PictureEvent import com.vitorpamplona.quartz.nip71Video.VideoEvent -import com.vitorpamplona.quartz.nip72ModCommunities.CommunityDefinitionEvent -import com.vitorpamplona.quartz.nip72ModCommunities.CommunityPostApprovalEvent +import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent +import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent -import com.vitorpamplona.quartz.nip89AppHandlers.AppDefinitionEvent +import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent import kotlinx.collections.immutable.toImmutableList @@ -474,7 +474,7 @@ private fun FullBleedNoteCompose( DisplayLocation(geo, nav) } - val baseReward = remember { noteEvent.getReward()?.let { Reward(it) } } + val baseReward = remember { noteEvent.bountyBaseReward()?.let { Reward(it) } } if (baseReward != null) { DisplayReward(baseReward, baseNote, accountViewModel, nav) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt index c69ee81a1..e0414782a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt @@ -103,7 +103,7 @@ import com.vitorpamplona.amethyst.ui.theme.Size40dp import com.vitorpamplona.amethyst.ui.theme.Size55dp import com.vitorpamplona.amethyst.ui.theme.VideoReactionColumnPadding import com.vitorpamplona.amethyst.ui.theme.placeholderText -import com.vitorpamplona.quartz.experimental.nip95.FileStorageHeaderEvent +import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent import com.vitorpamplona.quartz.nip68Picture.PictureEvent import com.vitorpamplona.quartz.nip71Video.VideoEvent import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent diff --git a/amethyst/src/main/res/values-cy-rGB/strings.xml b/amethyst/src/main/res/values-cy-rGB/strings.xml index 66a570486..3ea04e700 100644 --- a/amethyst/src/main/res/values-cy-rGB/strings.xml +++ b/amethyst/src/main/res/values-cy-rGB/strings.xml @@ -1,2 +1,2 @@ - + diff --git a/amethyst/src/main/res/values-da-rDK/strings.xml b/amethyst/src/main/res/values-da-rDK/strings.xml index 66a570486..3ea04e700 100644 --- a/amethyst/src/main/res/values-da-rDK/strings.xml +++ b/amethyst/src/main/res/values-da-rDK/strings.xml @@ -1,2 +1,2 @@ - + diff --git a/amethyst/src/main/res/values-en-rGB/strings.xml b/amethyst/src/main/res/values-en-rGB/strings.xml index 2e780a134..48dcf6594 100644 --- a/amethyst/src/main/res/values-en-rGB/strings.xml +++ b/amethyst/src/main/res/values-en-rGB/strings.xml @@ -1,5 +1,5 @@ - + Point to the QR Code Show QR Profile Image diff --git a/amethyst/src/main/res/values-gu-rIN/strings.xml b/amethyst/src/main/res/values-gu-rIN/strings.xml index 66a570486..3ea04e700 100644 --- a/amethyst/src/main/res/values-gu-rIN/strings.xml +++ b/amethyst/src/main/res/values-gu-rIN/strings.xml @@ -1,2 +1,2 @@ - + diff --git a/amethyst/src/main/res/values-hr-rHR/strings.xml b/amethyst/src/main/res/values-hr-rHR/strings.xml index 66a570486..3ea04e700 100644 --- a/amethyst/src/main/res/values-hr-rHR/strings.xml +++ b/amethyst/src/main/res/values-hr-rHR/strings.xml @@ -1,2 +1,2 @@ - + diff --git a/amethyst/src/main/res/values-iw-rIL/strings.xml b/amethyst/src/main/res/values-iw-rIL/strings.xml index 66a570486..3ea04e700 100644 --- a/amethyst/src/main/res/values-iw-rIL/strings.xml +++ b/amethyst/src/main/res/values-iw-rIL/strings.xml @@ -1,2 +1,2 @@ - + diff --git a/amethyst/src/main/res/values-kk-rKZ/strings.xml b/amethyst/src/main/res/values-kk-rKZ/strings.xml index 66a570486..3ea04e700 100644 --- a/amethyst/src/main/res/values-kk-rKZ/strings.xml +++ b/amethyst/src/main/res/values-kk-rKZ/strings.xml @@ -1,2 +1,2 @@ - + diff --git a/amethyst/src/main/res/values-ko-rKR/strings.xml b/amethyst/src/main/res/values-ko-rKR/strings.xml index 66a570486..3ea04e700 100644 --- a/amethyst/src/main/res/values-ko-rKR/strings.xml +++ b/amethyst/src/main/res/values-ko-rKR/strings.xml @@ -1,2 +1,2 @@ - + diff --git a/amethyst/src/main/res/values-ks-rIN/strings.xml b/amethyst/src/main/res/values-ks-rIN/strings.xml index 66a570486..3ea04e700 100644 --- a/amethyst/src/main/res/values-ks-rIN/strings.xml +++ b/amethyst/src/main/res/values-ks-rIN/strings.xml @@ -1,2 +1,2 @@ - + diff --git a/amethyst/src/main/res/values-lt-rLT/strings.xml b/amethyst/src/main/res/values-lt-rLT/strings.xml index 66a570486..3ea04e700 100644 --- a/amethyst/src/main/res/values-lt-rLT/strings.xml +++ b/amethyst/src/main/res/values-lt-rLT/strings.xml @@ -1,2 +1,2 @@ - + diff --git a/amethyst/src/main/res/values-ne-rNP/strings.xml b/amethyst/src/main/res/values-ne-rNP/strings.xml index 66a570486..3ea04e700 100644 --- a/amethyst/src/main/res/values-ne-rNP/strings.xml +++ b/amethyst/src/main/res/values-ne-rNP/strings.xml @@ -1,2 +1,2 @@ - + diff --git a/amethyst/src/main/res/values-pcm-rNG/strings.xml b/amethyst/src/main/res/values-pcm-rNG/strings.xml index 66a570486..3ea04e700 100644 --- a/amethyst/src/main/res/values-pcm-rNG/strings.xml +++ b/amethyst/src/main/res/values-pcm-rNG/strings.xml @@ -1,2 +1,2 @@ - + diff --git a/amethyst/src/main/res/values-pt-rPT/strings.xml b/amethyst/src/main/res/values-pt-rPT/strings.xml index 66a570486..3ea04e700 100644 --- a/amethyst/src/main/res/values-pt-rPT/strings.xml +++ b/amethyst/src/main/res/values-pt-rPT/strings.xml @@ -1,2 +1,2 @@ - + diff --git a/amethyst/src/main/res/values-ru-rUA/strings.xml b/amethyst/src/main/res/values-ru-rUA/strings.xml index 66a570486..3ea04e700 100644 --- a/amethyst/src/main/res/values-ru-rUA/strings.xml +++ b/amethyst/src/main/res/values-ru-rUA/strings.xml @@ -1,2 +1,2 @@ - + diff --git a/amethyst/src/main/res/values-sa-rIN/strings.xml b/amethyst/src/main/res/values-sa-rIN/strings.xml index 66a570486..3ea04e700 100644 --- a/amethyst/src/main/res/values-sa-rIN/strings.xml +++ b/amethyst/src/main/res/values-sa-rIN/strings.xml @@ -1,2 +1,2 @@ - + diff --git a/amethyst/src/main/res/values-sr-rSP/strings.xml b/amethyst/src/main/res/values-sr-rSP/strings.xml index 25197aa21..4e434e5ce 100644 --- a/amethyst/src/main/res/values-sr-rSP/strings.xml +++ b/amethyst/src/main/res/values-sr-rSP/strings.xml @@ -1,5 +1,5 @@ - + Покажите на КР код Прикажи КР слика профила diff --git a/amethyst/src/main/res/values-sw-rTZ/strings.xml b/amethyst/src/main/res/values-sw-rTZ/strings.xml index 66a570486..3ea04e700 100644 --- a/amethyst/src/main/res/values-sw-rTZ/strings.xml +++ b/amethyst/src/main/res/values-sw-rTZ/strings.xml @@ -1,2 +1,2 @@ - + diff --git a/amethyst/src/main/res/values-tr/strings.xml b/amethyst/src/main/res/values-tr/strings.xml index 0c9d8ae64..0cd4766e1 100644 --- a/amethyst/src/main/res/values-tr/strings.xml +++ b/amethyst/src/main/res/values-tr/strings.xml @@ -1,5 +1,5 @@ - + QR Code\'a yönlendir QR Göster Profil Resmi diff --git a/amethyst/src/main/res/values-uz-rUZ/strings.xml b/amethyst/src/main/res/values-uz-rUZ/strings.xml index 66a570486..3ea04e700 100644 --- a/amethyst/src/main/res/values-uz-rUZ/strings.xml +++ b/amethyst/src/main/res/values-uz-rUZ/strings.xml @@ -1,2 +1,2 @@ - + diff --git a/amethyst/src/main/res/values-vi-rVN/strings.xml b/amethyst/src/main/res/values-vi-rVN/strings.xml index d374dcabd..2392d4a79 100644 --- a/amethyst/src/main/res/values-vi-rVN/strings.xml +++ b/amethyst/src/main/res/values-vi-rVN/strings.xml @@ -1,5 +1,5 @@ - + Hình ảnh nhóm Hành vi trái pháp luật Không xác định diff --git a/amethyst/src/main/res/values-zh-rSG/strings.xml b/amethyst/src/main/res/values-zh-rSG/strings.xml index 66a570486..3ea04e700 100644 --- a/amethyst/src/main/res/values-zh-rSG/strings.xml +++ b/amethyst/src/main/res/values-zh-rSG/strings.xml @@ -1,2 +1,2 @@ - + diff --git a/amethyst/src/play/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationReceiverService.kt b/amethyst/src/play/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationReceiverService.kt index 84431be21..0335f8130 100644 --- a/amethyst/src/play/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationReceiverService.kt +++ b/amethyst/src/play/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationReceiverService.kt @@ -30,7 +30,7 @@ import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.getOrCreateDMChannel import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.getOrCreateZapChannel import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip59Giftwrap.GiftWrapEvent +import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModelTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModelTest.kt index e683d8d8b..19d667c7c 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModelTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModelTest.kt @@ -26,6 +26,7 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import io.mockk.MockKAnnotations import io.mockk.every @@ -70,7 +71,7 @@ class NewPostViewModelTest { every { accountViewModel.account } returns mockk() val textNoteEvent = mockk(relaxed = true) - every { textNoteEvent.mentions() } returns listOf("user1", "user2") + every { textNoteEvent.mentions() } returns listOf(PTag("user1"), PTag("user2")) every { replyingTo.event } returns textNoteEvent every { accountViewModel.userProfile() } returns mockk(relaxed = true) @@ -110,7 +111,7 @@ class NewPostViewModelTest { every { accountViewModel.account } returns mockk() val textNoteEvent = mockk(relaxed = true) - every { textNoteEvent.mentions() } returns listOf("") + every { textNoteEvent.mentions() } returns emptyList() every { replyingTo.event } returns textNoteEvent every { accountViewModel.userProfile() } returns mockk(relaxed = true) diff --git a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/NostrClient.kt b/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/NostrClient.kt index a0f681e05..70b61d5cc 100644 --- a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/NostrClient.kt +++ b/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/NostrClient.kt @@ -23,8 +23,8 @@ package com.vitorpamplona.ammolite.relays import android.util.Log import com.vitorpamplona.ammolite.service.checkNotInMainThread import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.relays.RelayState -import com.vitorpamplona.quartz.nip01Core.relays.sockets.WebsocketBuilderFactory +import com.vitorpamplona.quartz.nip01Core.relay.RelayState +import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebsocketBuilderFactory import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope diff --git a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/NostrDataSource.kt b/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/NostrDataSource.kt index ab513bc8e..f585f438f 100644 --- a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/NostrDataSource.kt +++ b/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/NostrDataSource.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.ammolite.relays import android.util.Log import com.vitorpamplona.ammolite.service.checkNotInMainThread import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.relays.RelayState +import com.vitorpamplona.quartz.nip01Core.relay.RelayState import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi diff --git a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/Relay.kt b/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/Relay.kt index 64007a254..2a6b27ad2 100644 --- a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/Relay.kt +++ b/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/Relay.kt @@ -21,11 +21,11 @@ package com.vitorpamplona.ammolite.relays import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.relays.RelayState -import com.vitorpamplona.quartz.nip01Core.relays.SimpleClientRelay -import com.vitorpamplona.quartz.nip01Core.relays.SubscriptionCollection -import com.vitorpamplona.quartz.nip01Core.relays.filters.Filter -import com.vitorpamplona.quartz.nip01Core.relays.sockets.WebsocketBuilderFactory +import com.vitorpamplona.quartz.nip01Core.relay.RelayState +import com.vitorpamplona.quartz.nip01Core.relay.SimpleClientRelay +import com.vitorpamplona.quartz.nip01Core.relay.SubscriptionCollection +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebsocketBuilderFactory import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent enum class FeedType { @@ -62,11 +62,11 @@ class RelaySubFilter( override fun getFilters(subscriptionId: String) = filter(subs.getSubscriptionFilters(subscriptionId)) - override fun allSubscriptions(): List = + override fun allSubscriptions(): List = subs.allSubscriptions().mapNotNull { filter -> val filters = filter(filter.value) if (filters.isNotEmpty()) { - com.vitorpamplona.quartz.nip01Core.relays + com.vitorpamplona.quartz.nip01Core.relay .Subscription(filter.key, filters) } else { null diff --git a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/RelayPool.kt b/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/RelayPool.kt index b228b74f5..303940f93 100644 --- a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/RelayPool.kt +++ b/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/RelayPool.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.ammolite.relays import androidx.compose.runtime.Immutable import com.vitorpamplona.ammolite.service.checkNotInMainThread import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.relays.RelayState +import com.vitorpamplona.quartz.nip01Core.relay.RelayState import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.channels.BufferOverflow diff --git a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/RelayStats.kt b/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/RelayStats.kt index bd2f728db..9c91ea374 100644 --- a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/RelayStats.kt +++ b/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/RelayStats.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.ammolite.relays -import com.vitorpamplona.quartz.nip01Core.relays.RelayStat +import com.vitorpamplona.quartz.nip01Core.relay.RelayStat object RelayStats { private val innerCache = mutableMapOf() diff --git a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/filters/IPerRelayFilter.kt b/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/filters/IPerRelayFilter.kt index 815d6bf24..f246b0219 100644 --- a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/filters/IPerRelayFilter.kt +++ b/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/filters/IPerRelayFilter.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.ammolite.relays.filters import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.relays.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter interface IPerRelayFilter { fun toRelay(forRelay: String): Filter diff --git a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/filters/SinceAuthorPerRelayFilter.kt b/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/filters/SinceAuthorPerRelayFilter.kt index 99d0fd4f8..1f6c57787 100644 --- a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/filters/SinceAuthorPerRelayFilter.kt +++ b/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/filters/SinceAuthorPerRelayFilter.kt @@ -24,9 +24,9 @@ import com.fasterxml.jackson.databind.node.JsonNodeFactory import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper -import com.vitorpamplona.quartz.nip01Core.relays.filters.Filter -import com.vitorpamplona.quartz.nip01Core.relays.filters.FilterMatcher -import com.vitorpamplona.quartz.nip01Core.relays.filters.FilterSerializer +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.filters.FilterMatcher +import com.vitorpamplona.quartz.nip01Core.relay.filters.FilterSerializer /** * This is a nostr filter with per-relay authors list and since parameters diff --git a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/filters/SincePerRelayFilter.kt b/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/filters/SincePerRelayFilter.kt index 486e61b82..a1ba84ae3 100644 --- a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/filters/SincePerRelayFilter.kt +++ b/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/filters/SincePerRelayFilter.kt @@ -23,9 +23,9 @@ package com.vitorpamplona.ammolite.relays.filters import com.fasterxml.jackson.databind.node.JsonNodeFactory import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper -import com.vitorpamplona.quartz.nip01Core.relays.filters.Filter -import com.vitorpamplona.quartz.nip01Core.relays.filters.FilterMatcher -import com.vitorpamplona.quartz.nip01Core.relays.filters.FilterSerializer +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.filters.FilterMatcher +import com.vitorpamplona.quartz.nip01Core.relay.filters.FilterSerializer /** * This is a nostr filter with per-relay authors list and since parameters diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/RichTextParserBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/RichTextParserBenchmark.kt index 1a2b38456..b46402730 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/RichTextParserBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/RichTextParserBenchmark.kt @@ -48,7 +48,7 @@ class RichTextParserBenchmark { assertNull( RichTextParser().createMediaContent( "https://github.com/vitorpamplona/amethyst/releases/download/v0.83.10/amethyst-googleplay-universal-v0.83.10.apk", - EmptyTagList, + emptyMap(), null, ), ) diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/CryptoBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/CryptoBenchmark.kt index 2f35046d0..a29c3ace4 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/CryptoBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/CryptoBenchmark.kt @@ -24,7 +24,7 @@ import androidx.benchmark.junit4.BenchmarkRule import androidx.benchmark.junit4.measureRepeated import androidx.test.ext.junit.runners.AndroidJUnit4 import com.vitorpamplona.quartz.CryptoUtils -import com.vitorpamplona.quartz.nip01Core.KeyPair +import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import junit.framework.TestCase.assertNotNull import org.junit.Rule import org.junit.Test diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/EventBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/EventBenchmark.kt index 495bae102..c3921abce 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/EventBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/EventBenchmark.kt @@ -24,8 +24,8 @@ import androidx.benchmark.junit4.BenchmarkRule import androidx.benchmark.junit4.measureRepeated import androidx.test.ext.junit.runners.AndroidJUnit4 import com.vitorpamplona.quartz.EventFactory -import com.vitorpamplona.quartz.nip01Core.hasValidSignature import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper +import com.vitorpamplona.quartz.nip01Core.verify import com.vitorpamplona.quartz.utils.TimeUtils import junit.framework.TestCase.assertTrue import org.junit.Rule @@ -60,7 +60,7 @@ class EventBenchmark { val event = EventMapper.fromJson(msg[2]) benchmarkRule.measureRepeated { // Should pass - assertTrue(event.hasValidSignature()) + assertTrue(event.verify()) } } diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/EventCmdHasherBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/EventCmdHasherBenchmark.kt index 9911848b4..f50b6db62 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/EventCmdHasherBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/EventCmdHasherBenchmark.kt @@ -25,8 +25,8 @@ import androidx.benchmark.junit4.measureRepeated import androidx.test.ext.junit.runners.AndroidJUnit4 import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.generateId -import com.vitorpamplona.quartz.nip01Core.hasCorrectIDHash import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper +import com.vitorpamplona.quartz.nip01Core.verifyId import junit.framework.TestCase.assertNotNull import junit.framework.TestCase.assertTrue import org.junit.Rule @@ -49,7 +49,7 @@ class EventCmdHasherBenchmark { benchmarkRule.measureRepeated { // Should pass - assertTrue(event.hasCorrectIDHash()) + assertTrue(event.verifyId()) } } @@ -58,7 +58,7 @@ class EventCmdHasherBenchmark { val event = Event.fromJson(largeKind1Event) benchmarkRule.measureRepeated { // Should pass - assertTrue(event.hasCorrectIDHash()) + assertTrue(event.verifyId()) } } diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapBenchmark.kt index f842bf2d0..024436e10 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapBenchmark.kt @@ -24,13 +24,15 @@ import androidx.benchmark.junit4.BenchmarkRule import androidx.benchmark.junit4.measureRepeated import androidx.test.ext.junit.runners.AndroidJUnit4 import com.vitorpamplona.quartz.CryptoUtils -import com.vitorpamplona.quartz.nip01Core.KeyPair import com.vitorpamplona.quartz.nip01Core.checkSignature import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip17Dm.NIP17Factory -import com.vitorpamplona.quartz.nip59Giftwrap.GiftWrapEvent -import com.vitorpamplona.quartz.nip59Giftwrap.SealedRumorEvent +import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent +import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent +import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import junit.framework.TestCase import org.junit.Assert import org.junit.Assert.assertTrue @@ -60,9 +62,11 @@ class GiftWrapBenchmark { var events: NIP17Factory.Result? = null val countDownLatch = CountDownLatch(1) - NIP17Factory().createMsgNIP17( - message, - listOf(receiver.pubKey), + NIP17Factory().createMessageNIP17( + ChatMessageEvent.build( + message, + listOf(PTag(receiver.pubKey)), + ), sender, ) { events = it @@ -110,9 +114,11 @@ class GiftWrapBenchmark { var giftWrap: GiftWrapEvent? = null val countDownLatch = CountDownLatch(1) - NIP17Factory().createMsgNIP17( - message, - listOf(receiver.pubKey), + NIP17Factory().createMessageNIP17( + ChatMessageEvent.build( + message, + listOf(PTag(receiver.pubKey)), + ), sender, ) { giftWrap = it.wraps.first() diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapReceivingBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapReceivingBenchmark.kt index 42bd2aed6..d1fc02a31 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapReceivingBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapReceivingBenchmark.kt @@ -24,17 +24,21 @@ import androidx.benchmark.junit4.BenchmarkRule import androidx.benchmark.junit4.measureRepeated import androidx.test.ext.junit.runners.AndroidJUnit4 import com.vitorpamplona.quartz.CryptoUtils -import com.vitorpamplona.quartz.nip01Core.KeyPair import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.hasCorrectIDHash -import com.vitorpamplona.quartz.nip01Core.hasVerifiedSignature +import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.hexToByteArray import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal -import com.vitorpamplona.quartz.nip17Dm.ChatMessageEvent -import com.vitorpamplona.quartz.nip59Giftwrap.GiftWrapEvent -import com.vitorpamplona.quartz.nip59Giftwrap.Rumor -import com.vitorpamplona.quartz.nip59Giftwrap.SealedRumorEvent +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.nip01Core.verifyId +import com.vitorpamplona.quartz.nip01Core.verifySignature +import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent +import com.vitorpamplona.quartz.nip17Dm.messages.changeSubject +import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning +import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiser +import com.vitorpamplona.quartz.nip59Giftwrap.rumors.Rumor +import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent +import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import junit.framework.TestCase.assertNotNull import junit.framework.TestCase.assertTrue import org.junit.Rule @@ -60,18 +64,18 @@ class GiftWrapReceivingBenchmark { val countDownLatch = CountDownLatch(1) var wrap: GiftWrapEvent? = null - ChatMessageEvent.create( - msg = "Hi there! This is a test message", - to = listOf(receiver.pubKey), - subject = "Party Tonight", - replyTos = emptyList(), - mentions = emptyList(), - zapReceiver = null, - markAsSensitive = true, - zapRaiserAmount = 10000, - geohash = null, - isDraft = true, - signer = sender, + sender.sign( + ChatMessageEvent.build( + msg = "Hi there! This is a test message", + to = + listOf( + PTag(receiver.pubKey), + ), + ) { + changeSubject("Party Tonight") + zapraiser(10000) + contentWarning("nsfw") + }, ) { SealedRumorEvent.create( event = it, @@ -100,18 +104,18 @@ class GiftWrapReceivingBenchmark { val countDownLatch = CountDownLatch(1) var seal: SealedRumorEvent? = null - ChatMessageEvent.create( - msg = "Hi there! This is a test message", - to = listOf(receiver.pubKey), - subject = "Party Tonight", - replyTos = emptyList(), - mentions = emptyList(), - zapReceiver = null, - markAsSensitive = true, - zapRaiserAmount = 10000, - geohash = null, - isDraft = true, - signer = sender, + sender.sign( + ChatMessageEvent.build( + msg = "Hi there! This is a test message", + to = + listOf( + PTag(receiver.pubKey), + ), + ) { + changeSubject("Party Tonight") + zapraiser(10000) + contentWarning("nsfw") + }, ) { SealedRumorEvent.create( event = it, @@ -145,7 +149,7 @@ class GiftWrapReceivingBenchmark { val wrap = createWrap(sender, receiver) - benchmarkRule.measureRepeated { wrap.hasCorrectIDHash() } + benchmarkRule.measureRepeated { wrap.verifyId() } } @Test @@ -155,7 +159,7 @@ class GiftWrapReceivingBenchmark { val wrap = createWrap(sender, receiver) - benchmarkRule.measureRepeated { wrap.hasVerifiedSignature() } + benchmarkRule.measureRepeated { wrap.verifySignature() } } @Test diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapSigningBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapSigningBenchmark.kt index f7cbb9306..5c1183c4e 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapSigningBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapSigningBenchmark.kt @@ -23,11 +23,15 @@ package com.vitorpamplona.quartz.benchmark import androidx.benchmark.junit4.BenchmarkRule import androidx.benchmark.junit4.measureRepeated import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.vitorpamplona.quartz.nip01Core.KeyPair +import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal -import com.vitorpamplona.quartz.nip17Dm.ChatMessageEvent -import com.vitorpamplona.quartz.nip59Giftwrap.GiftWrapEvent -import com.vitorpamplona.quartz.nip59Giftwrap.SealedRumorEvent +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent +import com.vitorpamplona.quartz.nip17Dm.messages.changeSubject +import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning +import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiser +import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent +import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import junit.framework.TestCase.assertTrue import org.junit.Rule import org.junit.Test @@ -53,18 +57,18 @@ class GiftWrapSigningBenchmark { benchmarkRule.measureRepeated { val countDownLatch = CountDownLatch(1) - ChatMessageEvent.create( - msg = "Hi there! This is a test message", - to = listOf(receiver.pubKey), - subject = "Party Tonight", - replyTos = emptyList(), - mentions = emptyList(), - zapReceiver = null, - markAsSensitive = true, - zapRaiserAmount = 10000, - geohash = null, - isDraft = false, - signer = sender, + sender.sign( + ChatMessageEvent.build( + msg = "Hi there! This is a test message", + to = + listOf( + PTag(receiver.pubKey), + ), + ) { + changeSubject("Party Tonight") + zapraiser(10000) + contentWarning("nsfw") + }, ) { countDownLatch.countDown() } @@ -82,18 +86,18 @@ class GiftWrapSigningBenchmark { var msg: ChatMessageEvent? = null - ChatMessageEvent.create( - msg = "Hi there! This is a test message", - to = listOf(receiver.pubKey), - subject = "Party Tonight", - replyTos = emptyList(), - mentions = emptyList(), - zapReceiver = null, - markAsSensitive = true, - zapRaiserAmount = 10000, - geohash = null, - isDraft = false, - signer = sender, + sender.sign( + ChatMessageEvent.build( + msg = "Hi there! This is a test message", + to = + listOf( + PTag(receiver.pubKey), + ), + ) { + changeSubject("Party Tonight") + zapraiser(10000) + contentWarning("nsfw") + }, ) { msg = it countDownLatch.countDown() @@ -124,18 +128,18 @@ class GiftWrapSigningBenchmark { var seal: SealedRumorEvent? = null - ChatMessageEvent.create( - msg = "Hi there! This is a test message", - to = listOf(receiver.pubKey), - subject = "Party Tonight", - replyTos = emptyList(), - mentions = emptyList(), - zapReceiver = null, - markAsSensitive = true, - zapRaiserAmount = 10000, - geohash = null, - isDraft = false, - signer = sender, + sender.sign( + ChatMessageEvent.build( + msg = "Hi there! This is a test message", + to = + listOf( + PTag(receiver.pubKey), + ), + ) { + changeSubject("Party Tonight") + zapraiser(10000) + contentWarning("nsfw") + }, ) { SealedRumorEvent.create( event = it, @@ -170,18 +174,18 @@ class GiftWrapSigningBenchmark { var wrap: GiftWrapEvent? = null - ChatMessageEvent.create( - msg = "Hi there! This is a test message", - to = listOf(receiver.pubKey), - subject = "Party Tonight", - replyTos = emptyList(), - mentions = emptyList(), - zapReceiver = null, - markAsSensitive = true, - zapRaiserAmount = 10000, - geohash = null, - isDraft = false, - signer = sender, + sender.sign( + ChatMessageEvent.build( + msg = "Hi there! This is a test message", + to = + listOf( + PTag(receiver.pubKey), + ), + ) { + changeSubject("Party Tonight") + zapraiser(10000) + contentWarning("nsfw") + }, ) { SealedRumorEvent.create( event = it, diff --git a/commons/src/main/java/com/vitorpamplona/amethyst/commons/richtext/MediaContentModels.kt b/commons/src/main/java/com/vitorpamplona/amethyst/commons/richtext/MediaContentModels.kt index 15255f8cd..94f03e820 100644 --- a/commons/src/main/java/com/vitorpamplona/amethyst/commons/richtext/MediaContentModels.kt +++ b/commons/src/main/java/com/vitorpamplona/amethyst/commons/richtext/MediaContentModels.kt @@ -21,13 +21,13 @@ package com.vitorpamplona.amethyst.commons.richtext import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip94FileMetadata.Dimension +import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag import java.io.File @Immutable abstract class BaseMediaContent( val description: String? = null, - val dim: Dimension? = null, + val dim: DimensionTag? = null, val blurhash: String? = null, ) @@ -36,7 +36,7 @@ abstract class MediaUrlContent( val url: String, description: String? = null, val hash: String? = null, - dim: Dimension? = null, + dim: DimensionTag? = null, blurhash: String? = null, val uri: String? = null, val mimeType: String? = null, @@ -48,7 +48,7 @@ open class MediaUrlImage( description: String? = null, hash: String? = null, blurhash: String? = null, - dim: Dimension? = null, + dim: DimensionTag? = null, uri: String? = null, val contentWarning: String? = null, mimeType: String? = null, @@ -59,7 +59,7 @@ class EncryptedMediaUrlImage( description: String? = null, hash: String? = null, blurhash: String? = null, - dim: Dimension? = null, + dim: DimensionTag? = null, uri: String? = null, contentWarning: String? = null, mimeType: String? = null, @@ -73,7 +73,7 @@ open class MediaUrlVideo( url: String, description: String? = null, hash: String? = null, - dim: Dimension? = null, + dim: DimensionTag? = null, uri: String? = null, val artworkUri: String? = null, val authorName: String? = null, @@ -87,7 +87,7 @@ class EncryptedMediaUrlVideo( url: String, description: String? = null, hash: String? = null, - dim: Dimension? = null, + dim: DimensionTag? = null, uri: String? = null, artworkUri: String? = null, authorName: String? = null, @@ -105,7 +105,7 @@ abstract class MediaPreloadedContent( description: String? = null, val mimeType: String? = null, val isVerified: Boolean? = null, - dim: Dimension? = null, + dim: DimensionTag? = null, blurhash: String? = null, val uri: String, val id: String? = null, @@ -118,7 +118,7 @@ class MediaLocalImage( localFile: File?, mimeType: String? = null, description: String? = null, - dim: Dimension? = null, + dim: DimensionTag? = null, blurhash: String? = null, isVerified: Boolean? = null, uri: String, @@ -129,7 +129,7 @@ class MediaLocalVideo( localFile: File?, mimeType: String? = null, description: String? = null, - dim: Dimension? = null, + dim: DimensionTag? = null, blurhash: String? = null, isVerified: Boolean? = null, uri: String, diff --git a/commons/src/main/java/com/vitorpamplona/amethyst/commons/richtext/RichTextParser.kt b/commons/src/main/java/com/vitorpamplona/amethyst/commons/richtext/RichTextParser.kt index c7b4584b7..a3325657b 100644 --- a/commons/src/main/java/com/vitorpamplona/amethyst/commons/richtext/RichTextParser.kt +++ b/commons/src/main/java/com/vitorpamplona/amethyst/commons/richtext/RichTextParser.kt @@ -27,10 +27,14 @@ import com.linkedin.urls.detection.UrlDetectorOptions import com.vitorpamplona.quartz.experimental.inlineMetadata.Nip54InlineMetadata import com.vitorpamplona.quartz.nip02FollowList.ImmutableListOfLists import com.vitorpamplona.quartz.nip30CustomEmoji.CustomEmoji -import com.vitorpamplona.quartz.nip36SensitiveContent.CONTENT_WARNING -import com.vitorpamplona.quartz.nip92IMeta.Nip92MediaAttachments -import com.vitorpamplona.quartz.nip94FileMetadata.Dimension -import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent +import com.vitorpamplona.quartz.nip31Alts.AltTag +import com.vitorpamplona.quartz.nip36SensitiveContent.ContentWarningTag +import com.vitorpamplona.quartz.nip92IMeta.IMetaTag +import com.vitorpamplona.quartz.nip92IMeta.imetasByUrl +import com.vitorpamplona.quartz.nip94FileMetadata.tags.BlurhashTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.HashTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.MimeTypeTag import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -46,14 +50,15 @@ import kotlin.coroutines.cancellation.CancellationException class RichTextParser { fun createMediaContent( fullUrl: String, - eventTags: ImmutableListOfLists, + eventTags: Map, description: String?, callbackUri: String? = null, ): MediaUrlContent? { val frags = Nip54InlineMetadata().parse(fullUrl) - val tags = Nip92MediaAttachments.parse(fullUrl, eventTags.lists) - val contentType = frags[FileHeaderEvent.MIME_TYPE] ?: tags[FileHeaderEvent.MIME_TYPE] + val tags = eventTags.get(fullUrl)?.properties ?: emptyMap() + + val contentType = frags[MimeTypeTag.TAG_NAME] ?: tags[MimeTypeTag.TAG_NAME]?.firstOrNull() val isImage: Boolean val isVideo: Boolean @@ -73,22 +78,22 @@ class RichTextParser { return if (isImage) { MediaUrlImage( url = fullUrl, - description = description ?: frags[FileHeaderEvent.ALT] ?: tags[FileHeaderEvent.ALT], - hash = frags[FileHeaderEvent.HASH] ?: tags[FileHeaderEvent.HASH], - blurhash = frags[FileHeaderEvent.BLUR_HASH] ?: tags[FileHeaderEvent.BLUR_HASH], - dim = frags[FileHeaderEvent.DIMENSION]?.let { Dimension.parse(it) } ?: tags[FileHeaderEvent.DIMENSION]?.let { Dimension.parse(it) }, - contentWarning = frags[CONTENT_WARNING] ?: tags[CONTENT_WARNING], + description = description ?: frags[AltTag.TAG_NAME] ?: tags[AltTag.TAG_NAME]?.firstOrNull(), + hash = frags[HashTag.TAG_NAME] ?: tags[HashTag.TAG_NAME]?.firstOrNull(), + blurhash = frags[BlurhashTag.TAG_NAME] ?: tags[BlurhashTag.TAG_NAME]?.firstOrNull(), + dim = frags[DimensionTag.TAG_NAME]?.let { DimensionTag.parse(it) } ?: tags[DimensionTag.TAG_NAME]?.firstOrNull()?.let { DimensionTag.parse(it) }, + contentWarning = frags[ContentWarningTag.TAG_NAME] ?: tags[ContentWarningTag.TAG_NAME]?.firstOrNull(), uri = callbackUri, mimeType = contentType, ) } else if (isVideo) { MediaUrlVideo( url = fullUrl, - description = description ?: frags[FileHeaderEvent.ALT] ?: tags[FileHeaderEvent.ALT], - hash = frags[FileHeaderEvent.HASH] ?: tags[FileHeaderEvent.HASH], - blurhash = frags[FileHeaderEvent.BLUR_HASH] ?: tags[FileHeaderEvent.BLUR_HASH], - dim = frags[FileHeaderEvent.DIMENSION]?.let { Dimension.parse(it) } ?: tags[FileHeaderEvent.DIMENSION]?.let { Dimension.parse(it) }, - contentWarning = frags[CONTENT_WARNING] ?: tags[CONTENT_WARNING], + description = description ?: frags[AltTag.TAG_NAME] ?: tags[AltTag.TAG_NAME]?.firstOrNull(), + hash = frags[HashTag.TAG_NAME] ?: tags[HashTag.TAG_NAME]?.firstOrNull(), + blurhash = frags[BlurhashTag.TAG_NAME] ?: tags[BlurhashTag.TAG_NAME]?.firstOrNull(), + dim = frags[DimensionTag.TAG_NAME]?.let { DimensionTag.parse(it) } ?: tags[DimensionTag.TAG_NAME]?.firstOrNull()?.let { DimensionTag.parse(it) }, + contentWarning = frags[ContentWarningTag.TAG_NAME] ?: tags[ContentWarningTag.TAG_NAME]?.firstOrNull(), uri = callbackUri, mimeType = contentType, ) @@ -131,10 +136,11 @@ class RichTextParser { tags: ImmutableListOfLists, callbackUri: String?, ): RichTextViewerState { + val imetas = tags.lists.imetasByUrl() val urlSet = parseValidUrls(content) val imagesForPager = - urlSet.mapNotNull { fullUrl -> createMediaContent(fullUrl, tags, content, callbackUri) }.associateBy { it.url } + urlSet.mapNotNull { fullUrl -> createMediaContent(fullUrl, imetas, content, callbackUri) }.associateBy { it.url } val emojiMap = CustomEmoji.createEmojiMap(tags) @@ -145,7 +151,7 @@ class RichTextParser { val imagesForPagerWithBase64 = imagesForPager + base64Images - .mapNotNull { createMediaContent(it.segmentText, tags, content, callbackUri) } + .mapNotNull { createMediaContent(it.segmentText, emptyMap(), content, callbackUri) } .associateBy { it.url } return RichTextViewerState( diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/CryptoUtilsTest.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/CryptoUtilsTest.kt index ababe2c5d..2b5b4862b 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/CryptoUtilsTest.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/CryptoUtilsTest.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.quartz import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.vitorpamplona.quartz.nip01Core.KeyPair +import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.hexToByteArray import com.vitorpamplona.quartz.nip01Core.toHexKey import org.junit.Assert.assertEquals diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/LargeDBSignatureCheck.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/LargeDBSignatureCheck.kt index a80729109..d86e208e2 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/LargeDBSignatureCheck.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/LargeDBSignatureCheck.kt @@ -24,8 +24,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation import com.fasterxml.jackson.module.kotlin.readValue import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.hasValidSignature import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper +import com.vitorpamplona.quartz.nip01Core.verify import junit.framework.TestCase.assertEquals import junit.framework.TestCase.assertTrue import kotlinx.coroutines.runBlocking @@ -48,7 +48,7 @@ class LargeDBSignatureCheck { var counter = 0 eventArray.forEach { - assertTrue(it.hasValidSignature()) + assertTrue(it.verify()) counter++ } @@ -69,7 +69,7 @@ class LargeDBSignatureCheck { var counter = 0 eventArray.forEach { if (it.sig != "") { - assertTrue(it.hasValidSignature()) + assertTrue(it.verify()) } counter++ } diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip01Core/Nip01Test.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip01Core/Nip01Test.kt index e4fa35b21..ce34592c7 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip01Core/Nip01Test.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip01Core/Nip01Test.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.quartz.nip01Core import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.vitorpamplona.quartz.nip01Core.crypto.Nip01 import com.vitorpamplona.quartz.utils.sha256Hash import fr.acinq.secp256k1.Secp256k1 import org.junit.Assert.assertEquals diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip03Timestamp/ots/OtsTest.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip03Timestamp/ots/OtsTest.kt index 05151a22f..7aa3095f8 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip03Timestamp/ots/OtsTest.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip03Timestamp/ots/OtsTest.kt @@ -20,20 +20,19 @@ */ package com.vitorpamplona.quartz.nip03Timestamp.ots -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.vitorpamplona.quartz.nip01Core.KeyPair import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent +import com.vitorpamplona.quartz.nip03Timestamp.OtsResolver import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertNotNull import junit.framework.TestCase.fail import org.junit.Assert import org.junit.Test -import org.junit.runner.RunWith import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit -@RunWith(AndroidJUnit4::class) class OtsTest { val otsEvent = "{\"content\":\"AE9wZW5UaW1lc3RhbXBzAABQcm9vZgC/ieLohOiSlAEIqCiiW0FlsU9lqK5f1A+cL6CGJ1Ah4V/A1yNJY/stUE3wECJz6ng/QxU5Z6xwaMx97qkI//AQqJv8bEMrGTplGWRv5qm4DgjxIHkcQqzpL0Fjr9VBAAijDe0IsQYpOhw1SIjZIgQa6i16CPEEZck7CvAIxR0AloJzCZoAg9/jDS75DI4uLWh0dHBzOi8vYWxpY2UuYnRjLmNhbGVuZGFyLm9wZW50aW1lc3RhbXBzLm9yZ//wEOwPtjIkKI1hmtv9t1kuxZcI8QRlyTsK8Ahl0wrCSggZzgCD3+MNLvkMjiwraHR0cHM6Ly9ib2IuYnRjLmNhbGVuZGFyLm9wZW50aW1lc3RhbXBzLm9yZ//wEE1dVGa8JCuf2ek0c5ybDKII8SCBoVz8Sal45Kd1O8STWIGJTcl5JPtAZBZitqk3BE9MqAjxBGXJOwrwCHoGVgAZi9q9AIPf4w0u+QyOKShodHRwczovL2Zpbm5leS5jYWxlbmRhci5ldGVybml0eXdhbGwuY29t8BAFZFXFYg7DJJ0OzjmJ0FKWCPEEZck7CvAI0M49IcBR5bf/AIPf4w0u+QyOIyJodHRwczovL2J0Yy5jYWxlbmRhci5jYXRhbGxheHkuY29tCPEgoE3IfYTmxxo4W/x/QYp/NGX6Wu93gSQkwbpjpOhZORcI8SDWLLurVQaXHdUuwivCfTfuxYCaq+AzypSGqLDAVocrEgjwIBfgjta16y13Gp4etQOCa9YiKEcM+/9AieG/vZolr3IDCPAgMR2zFCb384CEi8tVuI2fHgLT3I9zpe7oqJTzCcEqxWEI8SAJSdgeeosr7IxdOt8r7f0ipWc8FI6GAhgep8zSRgWikAjxIGxYmtCsC79Tx4z4YsT1WuMo+ycMkwhGQsQltF597cchCPAgCLrBf9vR1Aex6yY+vSkXAvLjMKdMqM/a1g8zNPwLeJcI8SBDCbTk4CczTuiIyZeUyYRVh31BZdjaSd2nU/pBQxu+6QjwIOwqE9/WqGC6CHH0i+tr7edvYX5PstSDf08KmnMqsqCoCPEgQIdEBg358vfek3Qjfyrgl51iCU6WUWmThsGLPDTcB0QI8SAX4dp64iI8pBx+zBqAQwUN6XgZ1cEfT8+2vha/9I1vzwjxWQEAAAAB8YJxvxJJth8OnxIV6UOXveIZAcJPTHcAkWgnucpuYqYAAAAAAP3///8CRhoMAAAAAAAWABTUUZK82uAvbU3vVyaaPIOddZBicwAAAAAAAAAAImog8ARCqgwACAjwIB9TcMDLhzgeS1Uw647lNvCfWECkkUvfrrOe6nay0sGdCAjwICYfs90sbPggoMICyOHGYbmOzop2L9mlnh4xqiBLY7yPCAjxINDiVWOBHnRmGJleQdB9myvJAJbNJ9kciZlTOkgJy89mCAjwIHfxqDLdwycj1Vtyth2CaSDdLQwiey9oV6Xov4stLpWNCAjxIGurJYpKJnKp9+y7MAdC+gXgHOiAu5P3RRUFW9l5hGaCCAjxICXqs9hdY0QMP/MNeqlt6s7xaIYtEXZ1CLvou5gaZNEICAjxIHdYxVeI76NXbT2zHcv6lw+v819Ooib7KWxc1GAsiX2fCAjwICPWdi3uBXOlIdmYi+V9C7wAqLyGE4DMoHD+GtvLizqiCAjwIOF2vENtWN5okEMMS+JSf1SGTY9yYP9j0JjXLbC1s+N1CAjxINWsgCtsPxhRNbe372k8/20WDbiL9e8934hGF256DvRECAjwII3mi+Li06j10ORxg0dYMkcsyGb115Jiqq1YEV3K/u+aCAgABYiWDXPXGQEDw9Qy\",\"created_at\":1707690688,\"id\":\"759f9da5846e936fab06766a524b36ba71c03bbc69ad0944fb8ee4bb1f3dd705\",\"kind\":1040,\"pubkey\":\"82fbb08c8ef45c4d71c88368d0ae805bc62fb92f166ab04a0b7a0c83d8cbc29a\",\"sig\":\"07c7896c8cbb97b5d7483097590c9d31b73f35c1ad9e752002bb5c1776cbd852e1d32704333d6930c9bc3e40f8b899a1f2e9f91cc3bf797d86acdecba7792576\",\"tags\":[[\"e\",\"a828a25b4165b14f65a8ae5fd40f9c2fa086275021e15fc0d7234963fb2d504d\"],[\"p\",\"595ca8eaace5899cb6ab7e2542bfc972136376f2eabc09287f1857eb8f167e53\"],[\"alt\",\"NIP-03 time stamp\"]]}" val otsEvent2 = "{\"content\":\"AE9wZW5UaW1lc3RhbXBzAABQcm9vZgC/ieLohOiSlAEIqGNPU2jhd4no+zg2ytDkuf5PIoivr8KHI8BL68aKGNbwENyCNtiEN98IzIZgEu3cl6YI//AQ6TkSRd3BTGhDHCK1KkJc+AjxIAHaizG++NNL3Vm13BJrIhT7Br6tEYpb0TVRGaadgiUMCPAgOSDREH9v1Y50UHu79LfC4Lcd9WklQJzRQpw+Unb/pyII8QRltDD58AgqrxfAVrLw7QCD3+MNLvkMji4taHR0cHM6Ly9hbGljZS5idGMuY2FsZW5kYXIub3BlbnRpbWVzdGFtcHMub3Jn//AQQMq/CLpGwY60nmddPS7OVgjxIDKxqd9nl+Mej41vP52Wd7gv7004r3n1rFGDObS8icRvCPAgH9TB/kwvXJEEw+h9Ce6fLaI3MORjtTEge0GbAefT6W4I8QRltDD58AhRcoU3gAo/swCD3+MNLvkMjiwraHR0cHM6Ly9ib2IuYnRjLmNhbGVuZGFyLm9wZW50aW1lc3RhbXBzLm9yZ//wECWtWsKo0uvSr8BYonjs3DEI8CBlsh2ng1Spl0K4oStYElGuMJsjd2uo5nXB+apo5A7ipwjxIM8oxynBwNA+QS/X7Ebtl1kyhFgfoOQioASNfCBzZ4gaCPEEZbQw+fAId6Yd5cw5gioAg9/jDS75DI4pKGh0dHBzOi8vZmlubmV5LmNhbGVuZGFyLmV0ZXJuaXR5d2FsbC5jb23wEJmPzXQbxv0AFTIyjTWjMskI8CAurbkrfrBtlinZXSDxj+m/oIkze57hGjTSxu1Xs87XYQjwIPk/LMD0zIgKoEE2dfeoYrrdHuO6dwmghTwUFajH2QzkCPEEZbQw+fAIE+Pq1/Wmdpj/AIPf4w0u+QyOIyJodHRwczovL2J0Yy5jYWxlbmRhci5jYXRhbGxheHkuY29tCPAgB2CbqkV7VpjRKIl3Ea6cBmB/EHcSN/YCgcc1E+mc07QI8CAfpkZ2Hh4Rukz3x4il3tZqQtlDlbna+I6so2t2YSEmMQjwIJOv32jbsMa2HJwpleRCKLEhgYOoHCSfpv1ZO0YNNNFsCPAgLMM7eFfCjokQfU4gdU5WpG/wBLkO9lDRF0GktL6ujt8I8SCRxJ0bC1PQ8qFmI/1jh8AS5d1/6VRJNMt1Hz41QmNr3QjwIBmgrKBF+OZ3y+XOMv2E7IZ4WwLr2u2H+ehsBfy7cPlICPEgm4ZMCSXzZVWu40d+zk2edaur6KOauo8X7V2KaFBR1VoI8SBKVVOiyq6IFqGn/15kLwk7L8upMAIZ0znjhYxYqSTQCQjwIMBD1twPZ33GxbwTiuOCeJPkoP++6R2wYpCii8UBTdgwCPAg84VkgMXwrt2xxRoeC1/6CtsFctki+w3m8Rs5/6g/IhEI8CCnzZQDhJyicX7bS7U8PMUObuC9Y4TXe+4THoXBMMXkxwjxIMZ0oAvshpcwowR3qPEDbwKZ6B4NPSU4Hz/+4PnD74gnCPAgIfLZEKqAvkMNXfakXoNq1UVqGSzL4Z86z5GzUfbvw8UI8SC8KoIeLvjd4vJ/xhNVphakPRd80YKeNkeYEuVH8k2EtAjwIPRtinLLxzt8iuw0XZtpTDzEstZOTNYVm+Bi3fEzdeIuCPFZAQAAAAE8vsasINN5DKon0KakX2HNdCB126ZLKXrw4PfvyEfqbwAAAAAA/f///wLPGw8AAAAAABYAFCvgxlh6msa4ZOtgvlc5KiZCx7IvAAAAAAAAAAAiaiDwBKygDAAICPEgB/2DJ3s6gMky/PceGZocTFRXjZUiCCAhHGYwQk/8yrUICPEgDuSd6+PJHUMuEHyLcKFxw7xfvRHRfInjkV3/Zy3BxqAICPEgZDgQ+4VXzlOIkGoO8EVxDgs2cWaeh4EEiaqa/y50gKAICPAgFI+zIuYcMF69GmPQVsXa9oy8eng7MeRZdIxArQyeX3oICPEgX5HYIuImpiSTEapgEssEW4l+W+4aRfNCG3pZf7z0hCoICPEgPkAbOSjFdtS4NT7MXgMYVQoQhI1JZtdFxUu4J3NTt7IICPEguW3qyuyGjctu5d9rM9P9ZCs/ZK4vAc+z21b9ygklgWAICPEgu4e2645xtvGhI1Zzuiv23vRhwE8uC9vj1TAgNg/C8UcICPAgwoQX8X0nY4HoQLRsJ0z8JCWQDzRh2iL2QXEb8z3gbjIICPAgzTwlPRtStsLJWhz3Q/0l8tMnrPSHVuh+zCiGk95dW2MICPAgGcBEuYZyzFNapHOfnJ9Q515QzO2VbIRhlVI0vIhd4jwICPAgidRMoM2pA+KmVJenVrLcbollsbUg9lL9bmv1C1dSxswICPAgZinGakwhbHdanTaRJeBkEUlbhfNokvj8b5KneyG+wzIICAAFiJYNc9cZAQOtwTI=\",\"created_at\":1706324334,\"id\":\"2ad074ddb7724eb13b4244b49cf2321b1057f37fdf8ce102e6329b839cf763a9\",\"kind\":1040,\"pubkey\":\"82fbb08c8ef45c4d71c88368d0ae805bc62fb92f166ab04a0b7a0c83d8cbc29a\",\"sig\":\"ad7274bb32ba9e9cfdbd52f4887e8a2fda1047c75a7185b2ab7ff254ebac14ed48a2b60737494d655e24c9400eeeec7e29293a77bfcaafaecd94b350c9a2c22b\",\"tags\":[[\"e\",\"a8634f5368e17789e8fb3836cad0e4b9fe4f2288afafc28723c04bebc68a18d6\"],[\"p\",\"c31e22c3715c1bde5608b7e0d04904f22f5fc453ba1806d21c9f2382e1e58c6c\"],[\"alt\",\"NIP-03 time stamp\"]]}" @@ -44,37 +43,39 @@ class OtsTest { @Test fun verifyNostrEvent() { val ots = Event.fromJson(otsEvent) as OtsEvent - println(ots.info()) + println(OtsResolver.info(ots.otsByteArray())) assertEquals(1707688818L, ots.verify()) } @Test fun verifyNostrEvent2() { val ots = Event.Companion.fromJson(otsEvent2) as OtsEvent - println(ots.info()) + println(OtsResolver.info(ots.otsByteArray())) assertEquals(1706322179L, ots.verify()) } @Test fun verifyNostrPendingEvent() { val ots = Event.Companion.fromJson(otsPendingEvent) as OtsEvent - println(ots.info()) + println(OtsResolver.info(ots.otsByteArray())) assertEquals(null, ots.verify()) val eventId = - ots.digestEvent() ?: run { + ots.digestEventId() ?: run { fail("Should not be null") return } - val upgraded = OtsEvent.upgrade(ots.content, eventId) + val upgraded = OtsEvent.upgrade(ots.otsByteArray(), eventId) + + assertNotNull(upgraded) val signer = NostrSignerInternal(KeyPair()) var newOts: OtsEvent? = null val countDownLatch = CountDownLatch(1) - OtsEvent.create(eventId, upgraded, signer) { + signer.sign(OtsEvent.build(eventId, upgraded!!)) { newOts = it countDownLatch.countDown() } @@ -82,7 +83,7 @@ class OtsTest { Assert.assertTrue(countDownLatch.await(1, TimeUnit.SECONDS)) println(newOts!!.toJson()) - println(newOts!!.info()) + println(OtsResolver.info(newOts!!.otsByteArray())) assertEquals(1708879025L, newOts!!.verify()) } @@ -94,7 +95,7 @@ class OtsTest { val countDownLatch = CountDownLatch(1) - OtsEvent.create(otsEvent2Digest, OtsEvent.stamp(otsEvent2Digest), signer) { + signer.sign(OtsEvent.build(otsEvent2Digest, OtsEvent.stamp(otsEvent2Digest))) { ots = it countDownLatch.countDown() } @@ -102,7 +103,7 @@ class OtsTest { Assert.assertTrue(countDownLatch.await(1, TimeUnit.SECONDS)) println(ots!!.toJson()) - println(ots!!.info()) + println(OtsResolver.info(ots!!.otsByteArray())) assertEquals(null, ots!!.verify()) } diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip04Dm/Nip04Test.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip04Dm/Nip04EncryptionTest.kt similarity index 79% rename from quartz/src/androidTest/java/com/vitorpamplona/quartz/nip04Dm/Nip04Test.kt rename to quartz/src/androidTest/java/com/vitorpamplona/quartz/nip04Dm/Nip04EncryptionTest.kt index 7a5ef8d5f..d024ca17a 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip04Dm/Nip04Test.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip04Dm/Nip04EncryptionTest.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.quartz.nip04Dm -import com.vitorpamplona.quartz.nip01Core.Nip01 +import com.vitorpamplona.quartz.nip01Core.crypto.Nip01 import com.vitorpamplona.quartz.nip01Core.hexToByteArray import com.vitorpamplona.quartz.nip01Core.toHexKey import fr.acinq.secp256k1.Secp256k1 @@ -29,10 +29,10 @@ import junit.framework.TestCase.assertTrue import org.junit.Test import java.security.SecureRandom -class Nip04Test { +class Nip04EncryptionTest { private val random = SecureRandom() private val nip01 = Nip01(Secp256k1.get(), random) - private val nip04 = Nip04(Secp256k1.get(), random) + private val nip04 = Nip04Encryption(Secp256k1.get(), random) val sk1 = "91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe".hexToByteArray() val sk2 = "96f6fa197aa07477ab88f6981118466ae3a982faab8ad5db9d5426870c73d220".hexToByteArray() @@ -89,17 +89,17 @@ class Nip04Test { @Test fun isNIP04Encode() { - assertTrue(Nip04.isNIP04("Xj/oZZolaItdyQ5v7xYFpA==?iv=+a6zagBp+mr5m1aFbHQ8lA==")) - assertTrue(Nip04.isNIP04("zJxfaJ32rN5Dg1ODjOlEew==?iv=EV5bUjcc4OX2Km/zPp4ndQ==")) + assertTrue(Nip04Encryption.isNIP04("Xj/oZZolaItdyQ5v7xYFpA==?iv=+a6zagBp+mr5m1aFbHQ8lA==")) + assertTrue(Nip04Encryption.isNIP04("zJxfaJ32rN5Dg1ODjOlEew==?iv=EV5bUjcc4OX2Km/zPp4ndQ==")) assertTrue( - Nip04.isNIP04("6f8dMstm+udOu7yipSn33orTmwQpWbtfuY95NH+eTU1kArysWJIDkYgI2D25EAGIDJsNd45jOJ2NbVOhFiL3ZP/NWsTwXokk34iyHyA/lkjzugQ1bHXoMD1fP/Ay4hB4al1NHb8HXHKZaxPrErwdRDb8qa/I6dXb/1xxyVvNQBHHvmsM5yIFaPwnCN1DZqXf2KbTA/Ekz7Hy+7R+Sy3TXLQDFpWYqykppkXc7Fs0qSuPRyxz5+anuN0dxZa9GTwTEnBrZPbthKkNRrvZMdTGJ6WumOh9aUq8OJJWy9aOgsXvs7qjN1UqcCqQqYaVnEOhCaqWNDsVtsFrVDj+SaLIBvCiomwF4C4nIgngJ5I69tx0UNI0q+ZnvOGQZ7m1PpW2NYP7Yw43HJNdeUEQAmdCPnh/PJwzLTnIxHmQU7n7SPlMdV0SFa6H8y2HHvex697GAkyE5t8c2uO24OnqIwF1tR3blIqXzTSRl0GA6QvrSj2p4UtnWjvF7xT7RiIEyTtgU/AsihTrXyXzWWZaIBJogpgw6erlZqWjCH7sZy/WoGYEiblobOAqMYxax6vRbeuGtoYksr/myX+x9rfLrYuoDRTw4woXOLmMrrj+Mf0TbAgc3SjdkqdsPU1553rlSqIEZXuFgoWmxvVQDtekgTYyS97G81TDSK9nTJT5ilku8NVq2LgtBXGwsNIw/xekcOUzJke3kpnFPutNaexR1VF3ohIuqRKYRGcd8ADJP2lfwMcaGRiplAmFoaVS1YUhQwYFNq9rMLf7YauRGV4BJg/t9srdGxf5RoKCvRo+XM/nLxxysTR9MVaEP/3lDqjwChMxs+eWfLHE5vRWV8hUEqdrWNZV29gsx5nQpzJ4PARGZVu310pQzc6JAlc2XAhhFk6RamkYJnmCSMnb/RblzIATBi2kNrCVAlaXIon188inB62rEpZGPkRIP7PUfu27S/elLQHBHeGDsxOXsBRo1gl3te+raoBHsxo6zvRnYbwdAQa5taDE63eh+fT6kFI+xYmXNAQkU8Dp0MVhEh4JQI06Ni/AKrvYpC95TXXIphZcF+/Pv/vaGkhG2X9S3uhugwWK?iv=2vWkOQQi0WynNJz/aZ4k2g=="), + Nip04Encryption.isNIP04("6f8dMstm+udOu7yipSn33orTmwQpWbtfuY95NH+eTU1kArysWJIDkYgI2D25EAGIDJsNd45jOJ2NbVOhFiL3ZP/NWsTwXokk34iyHyA/lkjzugQ1bHXoMD1fP/Ay4hB4al1NHb8HXHKZaxPrErwdRDb8qa/I6dXb/1xxyVvNQBHHvmsM5yIFaPwnCN1DZqXf2KbTA/Ekz7Hy+7R+Sy3TXLQDFpWYqykppkXc7Fs0qSuPRyxz5+anuN0dxZa9GTwTEnBrZPbthKkNRrvZMdTGJ6WumOh9aUq8OJJWy9aOgsXvs7qjN1UqcCqQqYaVnEOhCaqWNDsVtsFrVDj+SaLIBvCiomwF4C4nIgngJ5I69tx0UNI0q+ZnvOGQZ7m1PpW2NYP7Yw43HJNdeUEQAmdCPnh/PJwzLTnIxHmQU7n7SPlMdV0SFa6H8y2HHvex697GAkyE5t8c2uO24OnqIwF1tR3blIqXzTSRl0GA6QvrSj2p4UtnWjvF7xT7RiIEyTtgU/AsihTrXyXzWWZaIBJogpgw6erlZqWjCH7sZy/WoGYEiblobOAqMYxax6vRbeuGtoYksr/myX+x9rfLrYuoDRTw4woXOLmMrrj+Mf0TbAgc3SjdkqdsPU1553rlSqIEZXuFgoWmxvVQDtekgTYyS97G81TDSK9nTJT5ilku8NVq2LgtBXGwsNIw/xekcOUzJke3kpnFPutNaexR1VF3ohIuqRKYRGcd8ADJP2lfwMcaGRiplAmFoaVS1YUhQwYFNq9rMLf7YauRGV4BJg/t9srdGxf5RoKCvRo+XM/nLxxysTR9MVaEP/3lDqjwChMxs+eWfLHE5vRWV8hUEqdrWNZV29gsx5nQpzJ4PARGZVu310pQzc6JAlc2XAhhFk6RamkYJnmCSMnb/RblzIATBi2kNrCVAlaXIon188inB62rEpZGPkRIP7PUfu27S/elLQHBHeGDsxOXsBRo1gl3te+raoBHsxo6zvRnYbwdAQa5taDE63eh+fT6kFI+xYmXNAQkU8Dp0MVhEh4JQI06Ni/AKrvYpC95TXXIphZcF+/Pv/vaGkhG2X9S3uhugwWK?iv=2vWkOQQi0WynNJz/aZ4k2g=="), ) } @Test fun isNIP04EncodeWithBug() { assertTrue( - Nip04.isNIP04( + Nip04Encryption.isNIP04( "QOAYBWa88ConWs2C4kSvNqAcowCtg0ZRtAl7FyLSv9VMaJH4oCiDx0h8VLBnV97HdE4lv" + "TW7AYC1eEw8/t1dbe0qRc3XrOt7MrPAO8yqpy1/3lFB1+10kip0+KdgT8Quvv02wTP8Dqi" + "xpr2fliAIG2ONvDn+O5V0q9aVUN9HitgL/myTyR0T42edmxWeZoMBEOKvJyO80FekSsgVL" + diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip10Notes/CitationTests.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip10Notes/CitationTests.kt index 34ba5aaeb..749d5028e 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip10Notes/CitationTests.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip10Notes/CitationTests.kt @@ -21,82 +21,49 @@ package com.vitorpamplona.quartz.nip10Notes import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag +import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEventIds +import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEvents +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUserIds +import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUsers +import com.vitorpamplona.quartz.nip01Core.verify import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertTrue import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class CitationTests { - val json = - """ - { - "content": "Astral:\n\nhttps://void.cat/d/A5Fba5B1bcxwEmeyoD9nBs.webp\n\nIris:\n\nhttps://void.cat/d/44hTcVvhRps6xYYs99QsqA.webp\n\nSnort:\n\nhttps://void.cat/d/4nJD5TRePuQChM5tzteYbU.webp\n\nAmethyst agrees with Astral which I suspect are both wrong. nostr:npub13sx6fp3pxq5rl70x0kyfmunyzaa9pzt5utltjm0p8xqyafndv95q3saapa nostr:npub1v0lxxxxutpvrelsksy8cdhgfux9l6a42hsj2qzquu2zk7vc9qnkszrqj49 nostr:npub1g53mukxnjkcmr94fhryzkqutdz2ukq4ks0gvy5af25rgmwsl4ngq43drvk nostr:npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z ", - "created_at": 1683596206, - "id": "98b574c3527f0ffb30b7271084e3f07480733c7289f8de424d29eae82e36c758", - "kind": 1, - "pubkey": "46fcbe3065eaf1ae7811465924e48923363ff3f526bd6f73d7c184b16bd8ce4d", - "sig": "4aa5264965018fa12a326686ad3d3bd8beae3218dcc83689b19ca1e6baeb791531943c15363aa6707c7c0c8b2d601deca1f20c32078b2872d356cdca03b04cce", - "tags": [ - [ - "e", - "27ac621d7dc4a932e1a79f984308e7d20656dd6fddb2ce9cdfcb6a67b9a7bcc3", - "", - "root" - ], - [ - "e", - "be7245af96210a0dd048cab4ad38e52dbd6c09a53ea21a7edb6be8898e5727cc", - "", - "reply" - ], - [ - "p", - "22aa81510ee63fe2b16cae16e0921f78e9ba9882e2868e7e63ad6d08ae9b5954" - ], - [ - "p", - "22aa81510ee63fe2b16cae16e0921f78e9ba9882e2868e7e63ad6d08ae9b5954" - ], - [ - "p", - "3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24" - ], - [ - "p", - "ec4d241c334311b3a304433ee3442be29d0e88e7ec19b85edf2bba29b93565e2" - ], - [ - "p", - "0fe0b18b4dbf0e0aa40fcd47209b2a49b3431fc453b460efcf45ca0bd16bd6ac" - ], - [ - "p", - "8c0da4862130283ff9e67d889df264177a508974e2feb96de139804ea66d6168" - ], - [ - "p", - "63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed" - ], - [ - "p", - "4523be58d395b1b196a9b8c82b038b6895cb02b683d0c253a955068dba1facd0" - ], - [ - "p", - "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c" - ] - ], - "seenOn": [ - "wss://nostr.wine/" - ] -} -""" + val note = + TextNoteEvent( + "98b574c3527f0ffb30b7271084e3f07480733c7289f8de424d29eae82e36c758", + "46fcbe3065eaf1ae7811465924e48923363ff3f526bd6f73d7c184b16bd8ce4d", + 1683596206, + arrayOf( + arrayOf("e", "27ac621d7dc4a932e1a79f984308e7d20656dd6fddb2ce9cdfcb6a67b9a7bcc3", "", "root"), + arrayOf("e", "be7245af96210a0dd048cab4ad38e52dbd6c09a53ea21a7edb6be8898e5727cc", "", "reply"), + arrayOf("p", "22aa81510ee63fe2b16cae16e0921f78e9ba9882e2868e7e63ad6d08ae9b5954"), + arrayOf("p", "22aa81510ee63fe2b16cae16e0921f78e9ba9882e2868e7e63ad6d08ae9b5954"), + arrayOf("p", "3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24"), + arrayOf("p", "ec4d241c334311b3a304433ee3442be29d0e88e7ec19b85edf2bba29b93565e2"), + arrayOf("p", "0fe0b18b4dbf0e0aa40fcd47209b2a49b3431fc453b460efcf45ca0bd16bd6ac"), + arrayOf("p", "8c0da4862130283ff9e67d889df264177a508974e2feb96de139804ea66d6168"), + arrayOf("p", "63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed"), + arrayOf("p", "4523be58d395b1b196a9b8c82b038b6895cb02b683d0c253a955068dba1facd0"), + arrayOf("p", "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"), + ), + "Astral:\n\nhttps://void.cat/d/A5Fba5B1bcxwEmeyoD9nBs.webp\n\nIris:\n\nhttps://void.cat/d/44hTcVvhRps6xYYs99QsqA.webp\n\nSnort:\n\nhttps://void.cat/d/4nJD5TRePuQChM5tzteYbU.webp\n\nAmethyst agrees with Astral which I suspect are both wrong. nostr:npub13sx6fp3pxq5rl70x0kyfmunyzaa9pzt5utltjm0p8xqyafndv95q3saapa nostr:npub1v0lxxxxutpvrelsksy8cdhgfux9l6a42hsj2qzquu2zk7vc9qnkszrqj49 nostr:npub1g53mukxnjkcmr94fhryzkqutdz2ukq4ks0gvy5af25rgmwsl4ngq43drvk nostr:npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z ", + "4aa5264965018fa12a326686ad3d3bd8beae3218dcc83689b19ca1e6baeb791531943c15363aa6707c7c0c8b2d601deca1f20c32078b2872d356cdca03b04cce", + ) @Test - fun parseEvent() { - val event = EventMapper.fromJson(json) as TextNoteEvent + fun verifyEvent() { + assertTrue(note.verify()) + } + @Test + fun testCitedUsers() { val expectedCitations = setOf( "8c0da4862130283ff9e67d889df264177a508974e2feb96de139804ea66d6168", @@ -105,6 +72,75 @@ class CitationTests { "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", ) - assertEquals(expectedCitations, event.citedUsers()) + assertEquals(expectedCitations, note.citedUsers()) + } + + @Test + fun testTagsWithoutCitations() { + val expectedTagsWithoutCitations = + listOf( + "27ac621d7dc4a932e1a79f984308e7d20656dd6fddb2ce9cdfcb6a67b9a7bcc3", + "be7245af96210a0dd048cab4ad38e52dbd6c09a53ea21a7edb6be8898e5727cc", + ) + + assertEquals(expectedTagsWithoutCitations, note.tagsWithoutCitations()) + } + + @Test + fun testTaggedEventIds() { + val eventIds = + listOf( + "27ac621d7dc4a932e1a79f984308e7d20656dd6fddb2ce9cdfcb6a67b9a7bcc3", + "be7245af96210a0dd048cab4ad38e52dbd6c09a53ea21a7edb6be8898e5727cc", + ) + + assertEquals(eventIds, note.taggedEventIds()) + } + + @Test + fun testTaggedEvents() { + val eventIds = + listOf( + ETag("27ac621d7dc4a932e1a79f984308e7d20656dd6fddb2ce9cdfcb6a67b9a7bcc3"), + ETag("be7245af96210a0dd048cab4ad38e52dbd6c09a53ea21a7edb6be8898e5727cc"), + ) + + assertEquals(eventIds, note.taggedEvents()) + } + + @Test + fun testTaggedUsers() { + val eventIds = + listOf( + PTag("22aa81510ee63fe2b16cae16e0921f78e9ba9882e2868e7e63ad6d08ae9b5954"), + PTag("22aa81510ee63fe2b16cae16e0921f78e9ba9882e2868e7e63ad6d08ae9b5954"), + PTag("3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24"), + PTag("ec4d241c334311b3a304433ee3442be29d0e88e7ec19b85edf2bba29b93565e2"), + PTag("0fe0b18b4dbf0e0aa40fcd47209b2a49b3431fc453b460efcf45ca0bd16bd6ac"), + PTag("8c0da4862130283ff9e67d889df264177a508974e2feb96de139804ea66d6168"), + PTag("63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed"), + PTag("4523be58d395b1b196a9b8c82b038b6895cb02b683d0c253a955068dba1facd0"), + PTag("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"), + ) + + assertEquals(eventIds, note.taggedUsers()) + } + + @Test + fun testTaggedUserIds() { + val eventIds = + listOf( + "22aa81510ee63fe2b16cae16e0921f78e9ba9882e2868e7e63ad6d08ae9b5954", + "22aa81510ee63fe2b16cae16e0921f78e9ba9882e2868e7e63ad6d08ae9b5954", + "3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24", + "ec4d241c334311b3a304433ee3442be29d0e88e7ec19b85edf2bba29b93565e2", + "0fe0b18b4dbf0e0aa40fcd47209b2a49b3431fc453b460efcf45ca0bd16bd6ac", + "8c0da4862130283ff9e67d889df264177a508974e2feb96de139804ea66d6168", + "63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed", + "4523be58d395b1b196a9b8c82b038b6895cb02b683d0c253a955068dba1facd0", + "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", + ) + + assertEquals(eventIds, note.taggedUserIds()) } } diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip10Notes/ThreadingTests.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip10Notes/ThreadingTests.kt new file mode 100644 index 000000000..3a8888f00 --- /dev/null +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip10Notes/ThreadingTests.kt @@ -0,0 +1,340 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip10Notes + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUsers +import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag +import junit.framework.TestCase.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class ThreadingTests { + @Test + fun testLegacyEvent() { + val note = + TextNoteEvent( + "", + "", + 0, + arrayOf( + arrayOf("p", "4ca4f5533e40da5e0508796d409e6bb35a50b26fc304345617ab017183d83ac0"), + arrayOf("p", "534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec"), + arrayOf("p", "77ce56f89d1228f7ff3743ce1ad1b254857b9008564727ebd5a1f317362f6ca7"), + arrayOf("e", "89f220b63465c93542b1a78caa3a952cf4f196e91a50596493c8093c533ebc4d"), + arrayOf("e", "090c037b2e399ee74d9f134758928948dd9154413ca1a1acb37155046e03a051"), + arrayOf("e", "567b7c11f0fe582361e3cea6fcc7609a8942dfe196ee1b98d5604c93fbeea976"), + arrayOf("e", "49aff7ae6daeaaa2777931b90f9bb29f6cb01c5a3d7d88c8ba82d890f264afb4"), + arrayOf("e", "5e081ebb19153357d7c31e8a10b9ceeef29313f58dc8d701f66727fab02aef64"), + arrayOf("e", "bbd72f0ae14374aa8fb166b483cfcf99b57d7f4cf1600ccbf17c350040834631"), + arrayOf("e", "b857504288c18a15950dd05b9e8772c62ca6289d5aac373c0a8ee5b132e94e7c"), + ), + "", + "", + ) + + val taggedUsers = + listOf( + PTag("4ca4f5533e40da5e0508796d409e6bb35a50b26fc304345617ab017183d83ac0"), + PTag("534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec"), + PTag("77ce56f89d1228f7ff3743ce1ad1b254857b9008564727ebd5a1f317362f6ca7"), + ) + + assertEquals(taggedUsers, note.taggedUsers()) + + val expectedReply = MarkedETag("b857504288c18a15950dd05b9e8772c62ca6289d5aac373c0a8ee5b132e94e7c", null, MarkedETag.MARKER.REPLY) + + // check replies + assertEquals(null, note.markedReply()) + assertEquals(expectedReply, note.unmarkedReply()) + assertEquals(expectedReply, note.reply()) + + val replyTos = + listOf( + "89f220b63465c93542b1a78caa3a952cf4f196e91a50596493c8093c533ebc4d", + "090c037b2e399ee74d9f134758928948dd9154413ca1a1acb37155046e03a051", + "567b7c11f0fe582361e3cea6fcc7609a8942dfe196ee1b98d5604c93fbeea976", + "49aff7ae6daeaaa2777931b90f9bb29f6cb01c5a3d7d88c8ba82d890f264afb4", + "5e081ebb19153357d7c31e8a10b9ceeef29313f58dc8d701f66727fab02aef64", + "bbd72f0ae14374aa8fb166b483cfcf99b57d7f4cf1600ccbf17c350040834631", + "b857504288c18a15950dd05b9e8772c62ca6289d5aac373c0a8ee5b132e94e7c", + ) + + assertEquals(emptyList(), note.markedReplyTos()) + assertEquals(replyTos, note.unmarkedReplyTos()) + + assertEquals(expectedReply.eventId, note.replyingTo()) + assertEquals(expectedReply.eventId, note.replyingToAddressOrEvent()) + + // check root + val expectedRoot = MarkedETag("89f220b63465c93542b1a78caa3a952cf4f196e91a50596493c8093c533ebc4d", null, MarkedETag.MARKER.ROOT) + + assertEquals(null, note.markedRoot()) + assertEquals(expectedRoot, note.unmarkedRoot()) + assertEquals(expectedRoot, note.root()) + + // check quotes + assertEquals(emptySet(), note.citedUsers()) + assertEquals(emptySet(), note.findCitations()) + assertEquals(replyTos, note.tagsWithoutCitations()) + } + + @Test + fun testMarkedEvent() { + val note = + TextNoteEvent( + "", + "", + 0, + arrayOf( + arrayOf("p", "4ca4f5533e40da5e0508796d409e6bb35a50b26fc304345617ab017183d83ac0"), + arrayOf("p", "534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec"), + arrayOf("e", "77ce56f89d1228f7ff3743ce1ad1b254857b9008564727ebd5a1f317362f6ca7"), + arrayOf("e", "bbd72f0ae14374aa8fb166b483cfcf99b57d7f4cf1600ccbf17c350040834631", "", "root"), + arrayOf("e", "b857504288c18a15950dd05b9e8772c62ca6289d5aac373c0a8ee5b132e94e7c", "", "reply"), + ), + "", + "", + ) + + val taggedUsers = + listOf( + PTag("4ca4f5533e40da5e0508796d409e6bb35a50b26fc304345617ab017183d83ac0"), + PTag("534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec"), + ) + + assertEquals(taggedUsers, note.taggedUsers()) + + val expectedReply = MarkedETag("b857504288c18a15950dd05b9e8772c62ca6289d5aac373c0a8ee5b132e94e7c", null, MarkedETag.MARKER.REPLY) + val expectedReplyWrong = MarkedETag("77ce56f89d1228f7ff3743ce1ad1b254857b9008564727ebd5a1f317362f6ca7", null, MarkedETag.MARKER.REPLY) + + // check replies + assertEquals(expectedReply, note.markedReply()) + assertEquals(expectedReplyWrong, note.unmarkedReply()) + assertEquals(expectedReply, note.reply()) + + val markedReplies = + listOf( + "bbd72f0ae14374aa8fb166b483cfcf99b57d7f4cf1600ccbf17c350040834631", + "b857504288c18a15950dd05b9e8772c62ca6289d5aac373c0a8ee5b132e94e7c", + ) + + val unmarkedReplies = + listOf( + "77ce56f89d1228f7ff3743ce1ad1b254857b9008564727ebd5a1f317362f6ca7", + ) + + assertEquals(markedReplies, note.markedReplyTos()) + assertEquals(unmarkedReplies, note.unmarkedReplyTos()) + + assertEquals(expectedReply.eventId, note.replyingTo()) + assertEquals(expectedReply.eventId, note.replyingToAddressOrEvent()) + + // check root + val expectedRoot = MarkedETag("bbd72f0ae14374aa8fb166b483cfcf99b57d7f4cf1600ccbf17c350040834631", null, MarkedETag.MARKER.ROOT) + + val expectedRootWrong = MarkedETag("77ce56f89d1228f7ff3743ce1ad1b254857b9008564727ebd5a1f317362f6ca7", null, MarkedETag.MARKER.ROOT) + + assertEquals(expectedRoot, note.markedRoot()) + assertEquals(expectedRootWrong, note.unmarkedRoot()) + assertEquals(expectedRoot, note.root()) + + // check quotes + assertEquals(emptySet(), note.citedUsers()) + assertEquals(emptySet(), note.findCitations()) + assertEquals(markedReplies, note.tagsWithoutCitations()) + } + + @Test + fun testMarkedInverted() { + val note = + TextNoteEvent( + "", + "", + 0, + arrayOf( + arrayOf("p", "4ca4f5533e40da5e0508796d409e6bb35a50b26fc304345617ab017183d83ac0", "wss://goiaba.com"), + arrayOf("p", "534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec"), + arrayOf("p", "77ce56f89d1228f7ff3743ce1ad1b254857b9008564727ebd5a1f317362f6ca7"), + arrayOf("e", "5e081ebb19153357d7c31e8a10b9ceeef29313f58dc8d701f66727fab02aef64", "", "reply"), + arrayOf("e", "bbd72f0ae14374aa8fb166b483cfcf99b57d7f4cf1600ccbf17c350040834631", "wss://banana.com", "root", "4ca4f5533e40da5e0508796d409e6bb35a50b26fc304345617ab017183d83ac0"), + arrayOf("e", "b857504288c18a15950dd05b9e8772c62ca6289d5aac373c0a8ee5b132e94e7c"), + ), + "", + "", + ) + + val taggedUsers = + listOf( + PTag("4ca4f5533e40da5e0508796d409e6bb35a50b26fc304345617ab017183d83ac0", "wss://goiaba.com"), + PTag("534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec"), + PTag("77ce56f89d1228f7ff3743ce1ad1b254857b9008564727ebd5a1f317362f6ca7"), + ) + + assertEquals(taggedUsers, note.taggedUsers()) + + val expectedReply = MarkedETag("5e081ebb19153357d7c31e8a10b9ceeef29313f58dc8d701f66727fab02aef64", null, MarkedETag.MARKER.REPLY) + val expectedReplyWrong = MarkedETag("b857504288c18a15950dd05b9e8772c62ca6289d5aac373c0a8ee5b132e94e7c", null, MarkedETag.MARKER.REPLY) + + // check replies + assertEquals(expectedReply, note.markedReply()) + assertEquals(expectedReplyWrong, note.unmarkedReply()) + assertEquals(expectedReply, note.reply()) + + val markedReplies = + listOf( + "bbd72f0ae14374aa8fb166b483cfcf99b57d7f4cf1600ccbf17c350040834631", + "5e081ebb19153357d7c31e8a10b9ceeef29313f58dc8d701f66727fab02aef64", + ) + + val unmarkedReplies = + listOf( + "b857504288c18a15950dd05b9e8772c62ca6289d5aac373c0a8ee5b132e94e7c", + ) + + assertEquals(markedReplies, note.markedReplyTos()) + assertEquals(unmarkedReplies, note.unmarkedReplyTos()) + + assertEquals(expectedReply.eventId, note.replyingTo()) + assertEquals(expectedReply.eventId, note.replyingToAddressOrEvent()) + + // check root + val expectedRoot = MarkedETag("bbd72f0ae14374aa8fb166b483cfcf99b57d7f4cf1600ccbf17c350040834631", null, MarkedETag.MARKER.ROOT) + val expectedRootWrong = MarkedETag("b857504288c18a15950dd05b9e8772c62ca6289d5aac373c0a8ee5b132e94e7c", null, MarkedETag.MARKER.ROOT) + + assertEquals(expectedRoot, note.markedRoot()) + assertEquals(expectedRootWrong, note.unmarkedRoot()) + assertEquals(expectedRoot, note.root()) + + // check quotes + assertEquals(emptySet(), note.citedUsers()) + assertEquals(emptySet(), note.findCitations()) + assertEquals(markedReplies, note.tagsWithoutCitations()) + } + + @Test + fun testOnlyRoot() { + val note = + TextNoteEvent( + "", + "", + 0, + arrayOf( + arrayOf("p", "534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec", "wss://banana.com"), + arrayOf("e", "9abbfd9b9ac5ecdab45d14b8bf8d746139ea039e931a1b376d19a239f1946590", "", "root", "534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec"), + ), + "", + "", + ) + + val taggedUsers = + listOf( + PTag("534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec", "wss://banana.com"), + ) + + assertEquals(taggedUsers, note.taggedUsers()) + + val expectedReply = MarkedETag("9abbfd9b9ac5ecdab45d14b8bf8d746139ea039e931a1b376d19a239f1946590", null, MarkedETag.MARKER.REPLY) + + // check replies + assertEquals(null, note.markedReply()) + assertEquals(null, note.unmarkedReply()) + assertEquals(null, note.reply()) + + val replyTos = + listOf( + "9abbfd9b9ac5ecdab45d14b8bf8d746139ea039e931a1b376d19a239f1946590", + ) + + assertEquals(replyTos, note.markedReplyTos()) + assertEquals(emptyList(), note.unmarkedReplyTos()) + + assertEquals(expectedReply.eventId, note.replyingTo()) + assertEquals(expectedReply.eventId, note.replyingToAddressOrEvent()) + + // check root + val expectedRoot = MarkedETag("9abbfd9b9ac5ecdab45d14b8bf8d746139ea039e931a1b376d19a239f1946590", null, MarkedETag.MARKER.ROOT, "534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec") + + assertEquals(expectedRoot, note.markedRoot()) + assertEquals(null, note.unmarkedRoot()) + assertEquals(expectedRoot, note.root()) + + // check quotes + assertEquals(emptySet(), note.citedUsers()) + assertEquals(emptySet(), note.findCitations()) + assertEquals(replyTos, note.tagsWithoutCitations()) + } + + @Test + fun testOnlyReply() { + val note = + TextNoteEvent( + "", + "", + 0, + arrayOf( + arrayOf("p", "534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec", "wss://banana.com"), + arrayOf("e", "9abbfd9b9ac5ecdab45d14b8bf8d746139ea039e931a1b376d19a239f1946590", "", "reply", "534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec"), + ), + "", + "", + ) + + val taggedUsers = + listOf( + PTag("534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec", "wss://banana.com"), + ) + + assertEquals(taggedUsers, note.taggedUsers()) + + val expectedReply = MarkedETag("9abbfd9b9ac5ecdab45d14b8bf8d746139ea039e931a1b376d19a239f1946590", null, MarkedETag.MARKER.REPLY) + + // check replies + assertEquals(expectedReply, note.markedReply()) + assertEquals(null, note.unmarkedReply()) + assertEquals(expectedReply, note.reply()) + + val replyTos = + listOf( + "9abbfd9b9ac5ecdab45d14b8bf8d746139ea039e931a1b376d19a239f1946590", + ) + + assertEquals(replyTos, note.markedReplyTos()) + assertEquals(emptyList(), note.unmarkedReplyTos()) + + assertEquals(expectedReply.eventId, note.replyingTo()) + assertEquals(expectedReply.eventId, note.replyingToAddressOrEvent()) + + // check root + val expectedRoot = MarkedETag("9abbfd9b9ac5ecdab45d14b8bf8d746139ea039e931a1b376d19a239f1946590", null, MarkedETag.MARKER.ROOT, "534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec") + + assertEquals(null, note.markedRoot()) + assertEquals(null, note.unmarkedRoot()) + assertEquals(null, note.root()) + + // check quotes + assertEquals(emptySet(), note.citedUsers()) + assertEquals(emptySet(), note.findCitations()) + assertEquals(replyTos, note.tagsWithoutCitations()) + } +} diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip17Dm/AESGCMTest.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip17Dm/AESGCMTest.kt index 746196fb4..c75505987 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip17Dm/AESGCMTest.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip17Dm/AESGCMTest.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.quartz.nip17Dm import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation import com.vitorpamplona.quartz.nip01Core.hexToByteArray +import com.vitorpamplona.quartz.nip17Dm.files.encryption.AESGCM import junit.framework.TestCase.assertEquals import org.junit.Test import org.junit.runner.RunWith diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip17Dm/ChatroomKeyTest.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip17Dm/ChatroomKeyTest.kt index 747271afc..288ec5936 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip17Dm/ChatroomKeyTest.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip17Dm/ChatroomKeyTest.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.quartz.nip17Dm import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey import kotlinx.collections.immutable.persistentSetOf import org.junit.Assert.assertEquals import org.junit.Test diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip19Bech32/NIP19EmbedTests.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip19Bech32/NIP19EmbedTests.kt index 21640fde3..af9a55f99 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip19Bech32/NIP19EmbedTests.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip19Bech32/NIP19EmbedTests.kt @@ -22,11 +22,11 @@ package com.vitorpamplona.quartz.nip19Bech32 import androidx.test.ext.junit.runners.AndroidJUnit4 import com.vitorpamplona.quartz.experimental.medical.FhirResourceEvent -import com.vitorpamplona.quartz.nip01Core.KeyPair import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.hasValidSignature +import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.hexToByteArray import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal +import com.vitorpamplona.quartz.nip01Core.verify import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip19Bech32.entities.NEmbed import com.vitorpamplona.quartz.utils.Hex @@ -52,7 +52,9 @@ class NIP19EmbedTests { val countDownLatch = CountDownLatch(1) - TextNoteEvent.create("I like this. It could solve the ninvite problem in #1062, and it seems like it could be applied very broadly to limit the spread of events that shouldn't stand on their own or need to be private. The one question I have is how long are these embeds? If it's 50 lines of text, that breaks the human readable (or at least parseable) requirement of kind 1s. Also, encoding json in a tlv is silly, we should at least use the tlv to reduce the payload size.", isDraft = false, signer = signer) { + signer.sign( + TextNoteEvent.build("I like this. It could solve the ninvite problem in #1062, and it seems like it could be applied very broadly to limit the spread of events that shouldn't stand on their own or need to be private. The one question I have is how long are these embeds? If it's 50 lines of text, that breaks the human readable (or at least parseable) requirement of kind 1s. Also, encoding json in a tlv is silly, we should at least use the tlv to reduce the payload size."), + ) { textNote = it countDownLatch.countDown() } @@ -67,7 +69,7 @@ class NIP19EmbedTests { val decodedNote = (Nip19Parser.uriToRoute(bech32)?.entity as NEmbed).event - assertTrue(decodedNote.hasValidSignature()) + assertTrue(decodedNote.verify()) assertEquals(textNote!!.toJson(), decodedNote.toJson()) } @@ -83,7 +85,7 @@ class NIP19EmbedTests { val countDownLatch = CountDownLatch(1) - FhirResourceEvent.create(fhirPayload = visionPrescriptionFhir, signer = signer) { + signer.sign(FhirResourceEvent.build(visionPrescriptionFhir)) { eyeglassesPrescriptionEvent = it countDownLatch.countDown() } @@ -99,7 +101,7 @@ class NIP19EmbedTests { val decodedNote = (Nip19Parser.uriToRoute(bech32)?.entity as NEmbed).event - assertTrue(decodedNote.hasValidSignature()) + assertTrue(decodedNote.verify()) assertEquals(eyeglassesPrescriptionEvent!!.toJson(), decodedNote.toJson()) } @@ -115,7 +117,7 @@ class NIP19EmbedTests { val countDownLatch = CountDownLatch(1) - FhirResourceEvent.create(fhirPayload = visionPrescriptionBundle, signer = signer) { + signer.sign(FhirResourceEvent.build(visionPrescriptionBundle)) { eyeglassesPrescriptionEvent = it countDownLatch.countDown() } @@ -131,7 +133,7 @@ class NIP19EmbedTests { val decodedNote = (Nip19Parser.uriToRoute(bech32)?.entity as NEmbed).event - assertTrue(decodedNote.hasValidSignature()) + assertTrue(decodedNote.verify()) assertEquals(eyeglassesPrescriptionEvent!!.toJson(), decodedNote.toJson()) } @@ -147,7 +149,7 @@ class NIP19EmbedTests { val countDownLatch = CountDownLatch(1) - FhirResourceEvent.create(fhirPayload = visionPrescriptionBundle2, signer = signer) { + signer.sign(FhirResourceEvent.build(visionPrescriptionBundle2)) { eyeglassesPrescriptionEvent = it countDownLatch.countDown() } @@ -163,7 +165,7 @@ class NIP19EmbedTests { val decodedNote = (Nip19Parser.uriToRoute(bech32)?.entity as NEmbed).event - assertTrue(decodedNote.hasValidSignature()) + assertTrue(decodedNote.verify()) assertEquals(eyeglassesPrescriptionEvent!!.toJson(), decodedNote.toJson()) } diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip44Encryption/NIP44v2Test.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip44Encryption/NIP44v2Test.kt index 29e867baa..f603c98d3 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip44Encryption/NIP44v2Test.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip44Encryption/NIP44v2Test.kt @@ -23,8 +23,8 @@ package com.vitorpamplona.quartz.nip44Encryption import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -import com.vitorpamplona.quartz.nip01Core.KeyPair -import com.vitorpamplona.quartz.nip01Core.Nip01 +import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair +import com.vitorpamplona.quartz.nip01Core.crypto.Nip01 import com.vitorpamplona.quartz.nip01Core.hexToByteArray import com.vitorpamplona.quartz.nip01Core.toHexKey import com.vitorpamplona.quartz.utils.sha256Hash diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip46RemoteSigner/Nip46Test.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip46RemoteSigner/Nip46Test.kt index b7968c0f7..2050af360 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip46RemoteSigner/Nip46Test.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip46RemoteSigner/Nip46Test.kt @@ -21,10 +21,10 @@ package com.vitorpamplona.quartz.nip46RemoteSigner import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.vitorpamplona.quartz.nip01Core.KeyPair import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal -import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent +import com.vitorpamplona.quartz.nip02FollowList.ReadWrite import com.vitorpamplona.quartz.utils.TimeUtils import junit.framework.TestCase.assertEquals import org.junit.Ignore @@ -219,7 +219,7 @@ internal class Nip46Test { @Test fun testRelaysResponse() { - val expected = BunkerResponseGetRelays(relays = mapOf("url" to ContactListEvent.ReadWrite(true, false))) + val expected = BunkerResponseGetRelays(relays = mapOf("url" to ReadWrite(true, false))) val actual = encodeDecodeEvent(expected) assertEquals(expected.id, actual.id) diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip57Zaps/PrivateZapTests.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip57Zaps/PrivateZapTests.kt index d8a96b0b3..e872503a5 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip57Zaps/PrivateZapTests.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip57Zaps/PrivateZapTests.kt @@ -21,8 +21,8 @@ package com.vitorpamplona.quartz.nip57Zaps import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.vitorpamplona.quartz.nip01Core.KeyPair import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal import com.vitorpamplona.quartz.nip01Core.toHexKey diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip59GiftWraps/GiftWrapEventTest.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip59GiftWraps/GiftWrapEventTest.kt index 25a697ebb..9d4d50e3c 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip59GiftWraps/GiftWrapEventTest.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip59GiftWraps/GiftWrapEventTest.kt @@ -22,16 +22,17 @@ package com.vitorpamplona.quartz.nip59GiftWraps import androidx.test.ext.junit.runners.AndroidJUnit4 import com.vitorpamplona.quartz.nip01Core.HexKey -import com.vitorpamplona.quartz.nip01Core.KeyPair import com.vitorpamplona.quartz.nip01Core.checkSignature import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.hexToByteArray import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser -import com.vitorpamplona.quartz.nip17Dm.ChatMessageEvent import com.vitorpamplona.quartz.nip17Dm.NIP17Factory -import com.vitorpamplona.quartz.nip59Giftwrap.GiftWrapEvent -import com.vitorpamplona.quartz.nip59Giftwrap.SealedRumorEvent +import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent +import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent +import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import com.vitorpamplona.quartz.utils.Hex import org.junit.Assert.assertEquals import org.junit.Assert.assertNotEquals @@ -54,9 +55,11 @@ class GiftWrapEventTest { // Requires 3 tests val countDownLatch = CountDownLatch(3) - NIP17Factory().createMsgNIP17( - message, - listOf(receiver.pubKey), + NIP17Factory().createMessageNIP17( + ChatMessageEvent.build( + message, + listOf(PTag(receiver.pubKey, null)), + ), sender, ) { events -> countDownLatch.countDown() @@ -114,9 +117,11 @@ class GiftWrapEventTest { val countDownLatch = CountDownLatch(receivers.size + 2) - NIP17Factory().createMsgNIP17( - message, - receivers.map { it.pubKey }, + NIP17Factory().createMessageNIP17( + ChatMessageEvent.build( + message, + receivers.map { PTag(it.pubKey, null) }, + ), sender, ) { events -> countDownLatch.countDown() @@ -167,11 +172,11 @@ class GiftWrapEventTest { var giftWrapEventToSender: GiftWrapEvent? = null var giftWrapEventToReceiver: GiftWrapEvent? = null - ChatMessageEvent.create( - msg = "Hi There!", - isDraft = false, - to = listOf(receiver.pubKey), - signer = sender, + sender.sign( + ChatMessageEvent.build( + msg = "Hi There!", + to = listOf(PTag(receiver.pubKey, null)), + ), ) { senderMessage -> // MsgFor the Receiver @@ -306,11 +311,11 @@ class GiftWrapEventTest { var giftWrapEventToReceiverA: GiftWrapEvent? = null var giftWrapEventToReceiverB: GiftWrapEvent? = null - ChatMessageEvent.create( - msg = "Who is going to the party tonight?", - isDraft = false, - to = listOf(receiverA.pubKey, receiverB.pubKey), - signer = sender, + sender.sign( + ChatMessageEvent.build( + msg = "Who is going to the party tonight?", + to = listOf(PTag(receiverA.pubKey), PTag(receiverB.pubKey)), + ), ) { senderMessage -> SealedRumorEvent.create( event = senderMessage, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/CryptoUtils.kt b/quartz/src/main/java/com/vitorpamplona/quartz/CryptoUtils.kt index 08dab6a15..968ba94fa 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/CryptoUtils.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/CryptoUtils.kt @@ -21,8 +21,9 @@ package com.vitorpamplona.quartz import com.vitorpamplona.quartz.nip01Core.HexKey -import com.vitorpamplona.quartz.nip01Core.Nip01 -import com.vitorpamplona.quartz.nip04Dm.Nip04 +import com.vitorpamplona.quartz.nip01Core.crypto.Nip01 +import com.vitorpamplona.quartz.nip04Dm.Nip04Encoder +import com.vitorpamplona.quartz.nip04Dm.Nip04Encryption import com.vitorpamplona.quartz.nip06KeyDerivation.Nip06 import com.vitorpamplona.quartz.nip44Encryption.Nip44 import com.vitorpamplona.quartz.nip44Encryption.Nip44v2 @@ -38,7 +39,7 @@ object CryptoUtils { val nip01 = Nip01(secp256k1, random) val nip06 = Nip06(secp256k1) - val nip04 = Nip04(secp256k1, random) + val nip04 = Nip04Encryption(secp256k1, random) val nip44 = Nip44(secp256k1, random, nip04) val nip49 = Nip49(secp256k1, random) @@ -84,7 +85,7 @@ object CryptoUtils { privateKey: ByteArray, pubKey: ByteArray, ): String? = - if (Nip04.isNIP04(msg)) { + if (Nip04Encryption.isNIP04(msg)) { decryptNIP04(msg, privateKey, pubKey) } else { decryptNIP44(msg, privateKey, pubKey) @@ -100,7 +101,7 @@ object CryptoUtils { fun encryptNIP04( msg: String, sharedSecret: ByteArray, - ): Nip04.EncryptedInfo = nip04.encrypt(msg, sharedSecret) + ): Nip04Encoder = nip04.encrypt(msg, sharedSecret) fun decryptNIP04( msg: String, @@ -109,7 +110,7 @@ object CryptoUtils { ): String = nip04.decrypt(msg, privateKey, pubKey) fun decryptNIP04( - encryptedInfo: Nip04.EncryptedInfo, + encryptedInfo: Nip04Encoder, privateKey: ByteArray, pubKey: ByteArray, ): String = nip04.decrypt(encryptedInfo, privateKey, pubKey) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/EventFactory.kt b/quartz/src/main/java/com/vitorpamplona/quartz/EventFactory.kt index 78d830d3c..08b301031 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/EventFactory.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/EventFactory.kt @@ -22,49 +22,49 @@ package com.vitorpamplona.quartz import com.vitorpamplona.quartz.blossom.BlossomAuthorizationEvent import com.vitorpamplona.quartz.blossom.BlossomServersEvent -import com.vitorpamplona.quartz.experimental.audio.AudioHeaderEvent -import com.vitorpamplona.quartz.experimental.audio.AudioTrackEvent +import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent +import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent import com.vitorpamplona.quartz.experimental.edits.PrivateOutboxRelayListEvent import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryReadingStateEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStorySceneEvent import com.vitorpamplona.quartz.experimental.medical.FhirResourceEvent -import com.vitorpamplona.quartz.experimental.nip95.FileStorageEvent -import com.vitorpamplona.quartz.experimental.nip95.FileStorageHeaderEvent +import com.vitorpamplona.quartz.experimental.nip95.data.FileStorageEvent +import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent import com.vitorpamplona.quartz.experimental.nns.NNSEvent import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent import com.vitorpamplona.quartz.experimental.relationshipStatus.RelationshipStatusEvent import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent import com.vitorpamplona.quartz.nip01Core.EventHasher import com.vitorpamplona.quartz.nip01Core.HexKey -import com.vitorpamplona.quartz.nip01Core.MetadataEvent import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent import com.vitorpamplona.quartz.nip04Dm.PrivateDmEvent import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent -import com.vitorpamplona.quartz.nip17Dm.ChatMessageEncryptedFileHeaderEvent -import com.vitorpamplona.quartz.nip17Dm.ChatMessageEvent -import com.vitorpamplona.quartz.nip17Dm.ChatMessageRelayListEvent +import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent +import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent +import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent -import com.vitorpamplona.quartz.nip28PublicChat.ChannelCreateEvent -import com.vitorpamplona.quartz.nip28PublicChat.ChannelHideMessageEvent import com.vitorpamplona.quartz.nip28PublicChat.ChannelListEvent -import com.vitorpamplona.quartz.nip28PublicChat.ChannelMessageEvent -import com.vitorpamplona.quartz.nip28PublicChat.ChannelMetadataEvent -import com.vitorpamplona.quartz.nip28PublicChat.ChannelMuteUserEvent -import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiPackEvent -import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiPackSelectionEvent -import com.vitorpamplona.quartz.nip34Git.GitIssueEvent -import com.vitorpamplona.quartz.nip34Git.GitPatchEvent -import com.vitorpamplona.quartz.nip34Git.GitReplyEvent -import com.vitorpamplona.quartz.nip34Git.GitRepositoryEvent +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelHideMessageEvent +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMuteUserEvent +import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent +import com.vitorpamplona.quartz.nip30CustomEmoji.pack.EmojiPackEvent +import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiPackSelectionEvent +import com.vitorpamplona.quartz.nip34Git.issue.GitIssueEvent +import com.vitorpamplona.quartz.nip34Git.patch.GitPatchEvent +import com.vitorpamplona.quartz.nip34Git.reply.GitReplyEvent +import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent import com.vitorpamplona.quartz.nip37Drafts.DraftEvent @@ -82,8 +82,8 @@ import com.vitorpamplona.quartz.nip52Calendar.CalendarDateSlotEvent import com.vitorpamplona.quartz.nip52Calendar.CalendarEvent import com.vitorpamplona.quartz.nip52Calendar.CalendarRSVPEvent import com.vitorpamplona.quartz.nip52Calendar.CalendarTimeSlotEvent -import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesChatMessageEvent -import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesEvent +import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent import com.vitorpamplona.quartz.nip56Reports.ReportEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent @@ -92,28 +92,27 @@ import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent import com.vitorpamplona.quartz.nip58Badges.BadgeAwardEvent import com.vitorpamplona.quartz.nip58Badges.BadgeDefinitionEvent import com.vitorpamplona.quartz.nip58Badges.BadgeProfilesEvent -import com.vitorpamplona.quartz.nip59Giftwrap.GiftWrapEvent -import com.vitorpamplona.quartz.nip59Giftwrap.SealedRumorEvent +import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent +import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.nip68Picture.PictureEvent import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent -import com.vitorpamplona.quartz.nip71Video.VideoViewEvent -import com.vitorpamplona.quartz.nip72ModCommunities.CommunityDefinitionEvent import com.vitorpamplona.quartz.nip72ModCommunities.CommunityListEvent -import com.vitorpamplona.quartz.nip72ModCommunities.CommunityPostApprovalEvent +import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent +import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent import com.vitorpamplona.quartz.nip75ZapGoals.GoalEvent import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent -import com.vitorpamplona.quartz.nip89AppHandlers.AppDefinitionEvent -import com.vitorpamplona.quartz.nip89AppHandlers.AppRecommendationEvent +import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent +import com.vitorpamplona.quartz.nip89AppHandlers.recommendation.AppRecommendationEvent import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryRequestEvent import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryResponseEvent import com.vitorpamplona.quartz.nip90Dvms.NIP90StatusEvent import com.vitorpamplona.quartz.nip90Dvms.NIP90UserDiscoveryRequestEvent import com.vitorpamplona.quartz.nip90Dvms.NIP90UserDiscoveryResponseEvent import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent -import com.vitorpamplona.quartz.nip96FileStorage.FileServersEvent +import com.vitorpamplona.quartz.nip96FileStorage.config.FileServersEvent import com.vitorpamplona.quartz.nip98HttpAuth.HTTPAuthorizationEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent @@ -251,7 +250,6 @@ class EventFactory { TorrentCommentEvent.KIND -> TorrentCommentEvent(id, pubKey, createdAt, tags, content, sig) VideoHorizontalEvent.KIND -> VideoHorizontalEvent(id, pubKey, createdAt, tags, content, sig) VideoVerticalEvent.KIND -> VideoVerticalEvent(id, pubKey, createdAt, tags, content, sig) - VideoViewEvent.KIND -> VideoViewEvent(id, pubKey, createdAt, tags, content, sig) WikiNoteEvent.KIND -> WikiNoteEvent(id, pubKey, createdAt, tags, content, sig) else -> { factories[kind]?.let { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/blossom/BlossomServersEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/blossom/BlossomServersEvent.kt index 0c0e265d8..822a634aa 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/blossom/BlossomServersEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/blossom/BlossomServersEvent.kt @@ -25,7 +25,7 @@ import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.BaseReplaceableEvent import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer +import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.utils.TimeUtils @Immutable @@ -58,7 +58,7 @@ class BlossomServersEvent( servers .map { arrayOf("server", it) - }.plusElement(AltTagSerializer.toTagArray(ALT)) + }.plusElement(AltTag.assemble(ALT)) .toTypedArray() fun updateRelayList( diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/AudioHeaderEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/AudioHeaderEvent.kt deleted file mode 100644 index 6326bd253..000000000 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/AudioHeaderEvent.kt +++ /dev/null @@ -1,88 +0,0 @@ -/** - * Copyright (c) 2024 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.quartz.experimental.audio - -import androidx.compose.runtime.Immutable -import com.fasterxml.jackson.module.kotlin.readValue -import com.vitorpamplona.quartz.nip01Core.HexKey -import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer -import com.vitorpamplona.quartz.nip36SensitiveContent.ContentWarningSerializer -import com.vitorpamplona.quartz.utils.TimeUtils - -@Immutable -class AudioHeaderEvent( - id: HexKey, - pubKey: HexKey, - createdAt: Long, - tags: Array>, - content: String, - sig: HexKey, -) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { - fun download() = tags.firstOrNull { it.size > 1 && it[0] == DOWNLOAD_URL }?.get(1) - - fun stream() = tags.firstOrNull { it.size > 1 && it[0] == STREAM_URL }?.get(1) - - fun wavefrom() = - tags - .firstOrNull { it.size > 1 && it[0] == WAVEFORM } - ?.get(1) - ?.let { EventMapper.mapper.readValue>(it) } - - companion object { - const val KIND = 1808 - const val ALT = "Audio header" - - private const val DOWNLOAD_URL = "download_url" - private const val STREAM_URL = "stream_url" - private const val WAVEFORM = "waveform" - - fun create( - description: String, - downloadUrl: String, - streamUrl: String? = null, - wavefront: String? = null, - sensitiveContent: Boolean? = null, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (AudioHeaderEvent) -> Unit, - ) { - val tags = - listOfNotNull( - downloadUrl.let { arrayOf(DOWNLOAD_URL, it) }, - streamUrl?.let { arrayOf(STREAM_URL, it) }, - wavefront?.let { arrayOf(WAVEFORM, it) }, - sensitiveContent?.let { - if (it) { - ContentWarningSerializer.toTagArray() - } else { - null - } - }, - AltTagSerializer.toTagArray(ALT), - ).toTypedArray() - - signer.sign(createdAt, KIND, tags, description, onReady) - } - } -} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/header/AudioHeaderEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/header/AudioHeaderEvent.kt new file mode 100644 index 000000000..d012db416 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/header/AudioHeaderEvent.kt @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.experimental.audio.header + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.experimental.audio.header.tags.DownloadUrlTag +import com.vitorpamplona.quartz.experimental.audio.header.tags.StreamUrlTag +import com.vitorpamplona.quartz.experimental.audio.header.tags.WaveformTag +import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip31Alts.alt +import com.vitorpamplona.quartz.utils.TimeUtils + +@Immutable +class AudioHeaderEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { + fun download() = tags.firstNotNullOfOrNull(DownloadUrlTag::parse) + + fun stream() = tags.firstNotNullOfOrNull(StreamUrlTag::parse) + + fun wavefrom() = tags.firstNotNullOfOrNull(WaveformTag::parse) + + companion object { + const val KIND = 1808 + const val ALT = "Audio header" + + fun build( + description: String, + downloadUrl: String, + streamUrl: String? = null, + wavefront: List? = null, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, description, createdAt) { + alt(ALT) + downloadUrl.let { downloadUrl(it) } + streamUrl?.let { streamUrl(it) } + wavefront?.let { wavefront(it) } + initializer() + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/header/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/header/TagArrayBuilderExt.kt new file mode 100644 index 000000000..78280af77 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/header/TagArrayBuilderExt.kt @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.experimental.audio.header + +import com.vitorpamplona.quartz.experimental.audio.header.tags.DownloadUrlTag +import com.vitorpamplona.quartz.experimental.audio.header.tags.StreamUrlTag +import com.vitorpamplona.quartz.experimental.audio.header.tags.WaveformTag +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder + +fun TagArrayBuilder.downloadUrl(downloadUrlTag: String) = addUnique(DownloadUrlTag.assemble(downloadUrlTag)) + +fun TagArrayBuilder.streamUrl(streamUrl: String) = addUnique(StreamUrlTag.assemble(streamUrl)) + +fun TagArrayBuilder.wavefront(wave: List) = addUnique(WaveformTag.assemble(wave)) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/header/tags/DownloadUrlTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/header/tags/DownloadUrlTag.kt new file mode 100644 index 000000000..5f335b67b --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/header/tags/DownloadUrlTag.kt @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.experimental.audio.header.tags + +class DownloadUrlTag { + companion object { + const val TAG_NAME = "download_url" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): String? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1] + } + + @JvmStatic + fun assemble(url: String) = arrayOf(TAG_NAME, url) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/header/tags/StreamUrlTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/header/tags/StreamUrlTag.kt new file mode 100644 index 000000000..d3bc35de7 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/header/tags/StreamUrlTag.kt @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.experimental.audio.header.tags + +class StreamUrlTag { + companion object { + const val TAG_NAME = "stream_url" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): String? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1] + } + + @JvmStatic + fun assemble(url: String) = arrayOf(TAG_NAME, url) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/header/tags/WaveformTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/header/tags/WaveformTag.kt new file mode 100644 index 000000000..0942299f2 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/header/tags/WaveformTag.kt @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.experimental.audio.header.tags + +import com.fasterxml.jackson.module.kotlin.readValue +import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper + +class WaveformTag( + val wave: List, +) { + fun toTagArray() = assemble(wave) + + companion object { + const val TAG_NAME = "waveform" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): WaveformTag? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + if (tag[1].isEmpty()) return null + val wave = runCatching { EventMapper.mapper.readValue>(tag[1]) }.getOrNull() + if (wave.isNullOrEmpty()) return null + return WaveformTag(wave) + } + + @JvmStatic + fun assemble(wave: List) = arrayOf(TAG_NAME, EventMapper.mapper.writeValueAsString(wave)) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/AudioTrackEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/track/AudioTrackEvent.kt similarity index 54% rename from quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/AudioTrackEvent.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/track/AudioTrackEvent.kt index 25c04be6c..d7eccfad5 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/AudioTrackEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/track/AudioTrackEvent.kt @@ -18,14 +18,22 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.experimental.audio +package com.vitorpamplona.quartz.experimental.audio.track import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.experimental.audio.track.tags.CoverTag +import com.vitorpamplona.quartz.experimental.audio.track.tags.MediaTag +import com.vitorpamplona.quartz.experimental.audio.track.tags.ParticipantTag +import com.vitorpamplona.quartz.experimental.audio.track.tags.PriceTag +import com.vitorpamplona.quartz.experimental.audio.track.tags.TypeTag import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag +import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.utils.TimeUtils +import java.util.UUID @Immutable class AudioTrackEvent( @@ -36,53 +44,40 @@ class AudioTrackEvent( content: String, sig: HexKey, ) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) { - fun participants() = tags.filter { it.size > 1 && it[0] == "p" }.map { Participant(it[1], it.getOrNull(2)) } + fun participants() = tags.mapNotNull(ParticipantTag::parse) - fun type() = tags.firstOrNull { it.size > 1 && it[0] == TYPE }?.get(1) + fun type() = tags.firstNotNullOfOrNull(TypeTag::parse) - fun price() = tags.firstOrNull { it.size > 1 && it[0] == PRICE }?.get(1) + fun price() = tags.firstNotNullOfOrNull(PriceTag::parse) - fun cover() = tags.firstOrNull { it.size > 1 && it[0] == COVER }?.get(1) + fun cover() = tags.firstNotNullOfOrNull(CoverTag::parse) - // fun subject() = tags.firstOrNull { it.size > 1 && it[0] == SUBJECT }?.get(1) - fun media() = tags.firstOrNull { it.size > 1 && it[0] == MEDIA }?.get(1) + fun media() = tags.firstNotNullOfOrNull(MediaTag::parse) companion object { const val KIND = 31337 - const val ALT = "Audio track" + const val ALT_DESCRIPTION = "Audio track" - private const val TYPE = "c" - private const val PRICE = "price" - private const val COVER = "cover" - private const val SUBJECT = "subject" - private const val MEDIA = "media" - - fun create( + fun build( type: String, media: String, price: String? = null, cover: String? = null, subject: String? = null, - signer: NostrSigner, + dTag: String = UUID.randomUUID().toString(), createdAt: Long = TimeUtils.now(), - onReady: (AudioTrackEvent) -> Unit, - ) { - val tags = - listOfNotNull( - arrayOf(MEDIA, media), - arrayOf(TYPE, type), - price?.let { arrayOf(PRICE, it) }, - cover?.let { arrayOf(COVER, it) }, - subject?.let { arrayOf(SUBJECT, it) }, - AltTagSerializer.toTagArray(ALT), - ).toTypedArray() + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, "", createdAt) { + dTag(dTag) + alt(ALT_DESCRIPTION) - signer.sign(createdAt, KIND, tags, "", onReady) + type(type) + media(media) + price?.let { price(it) } + cover?.let { cover(it) } + subject?.let { subject(it) } + + initializer() } } } - -@Immutable data class Participant( - val key: String, - val role: String?, -) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/track/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/track/TagArrayBuilderExt.kt new file mode 100644 index 000000000..027b02c9a --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/track/TagArrayBuilderExt.kt @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.experimental.audio.track + +import com.vitorpamplona.quartz.experimental.audio.track.tags.CoverTag +import com.vitorpamplona.quartz.experimental.audio.track.tags.MediaTag +import com.vitorpamplona.quartz.experimental.audio.track.tags.PriceTag +import com.vitorpamplona.quartz.experimental.audio.track.tags.TypeTag +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip14Subject.SubjectTag + +fun TagArrayBuilder.price(price: String) = addUnique(PriceTag.assemble(price)) + +fun TagArrayBuilder.cover(coverUrl: String) = addUnique(CoverTag.assemble(coverUrl)) + +fun TagArrayBuilder.media(mediaUrl: String) = addUnique(MediaTag.assemble(mediaUrl)) + +fun TagArrayBuilder.type(type: String) = addUnique(TypeTag.assemble(type)) + +fun TagArrayBuilder.subject(subject: String) = addUnique(SubjectTag.assemble(subject)) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/track/tags/CoverTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/track/tags/CoverTag.kt new file mode 100644 index 000000000..e52b81d2e --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/track/tags/CoverTag.kt @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.experimental.audio.track.tags + +class CoverTag { + companion object { + const val TAG_NAME = "cover" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): String? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1] + } + + @JvmStatic + fun assemble(url: String) = arrayOf(TAG_NAME, url) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/track/tags/MediaTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/track/tags/MediaTag.kt new file mode 100644 index 000000000..d620dbe45 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/track/tags/MediaTag.kt @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.experimental.audio.track.tags + +class MediaTag { + companion object { + const val TAG_NAME = "media" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): String? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1] + } + + @JvmStatic + fun assemble(url: String) = arrayOf(TAG_NAME, url) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/track/tags/ParticipantTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/track/tags/ParticipantTag.kt new file mode 100644 index 000000000..155f0d919 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/track/tags/ParticipantTag.kt @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.experimental.audio.track.tags + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.Tag +import com.vitorpamplona.quartz.nip01Core.core.isNotName +import com.vitorpamplona.quartz.nip01Core.tags.people.PubKeyReferenceTag +import com.vitorpamplona.quartz.utils.arrayOfNotNull + +@Immutable +data class ParticipantTag( + override val pubKey: String, + override val relayHint: String?, +) : PubKeyReferenceTag { + fun toTagArray() = assemble(pubKey, relayHint) + + companion object { + const val TAG_NAME = "p" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Tag): ParticipantTag? { + if (tag.isNotName(TAG_NAME, TAG_SIZE)) return null + if (tag[1].length != 64) return null + return ParticipantTag(tag[1], tag.getOrNull(2)) + } + + @JvmStatic + fun parseKey(tag: Tag): String? { + if (tag.isNotName(TAG_NAME, TAG_SIZE)) return null + if (tag[1].length != 64) return null + return tag[1] + } + + @JvmStatic + fun assemble( + pubkey: HexKey, + relayHint: String?, + ) = arrayOfNotNull(TAG_NAME, pubkey, relayHint) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/track/tags/PriceTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/track/tags/PriceTag.kt new file mode 100644 index 000000000..c0439e8df --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/track/tags/PriceTag.kt @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.experimental.audio.track.tags + +class PriceTag { + companion object { + const val TAG_NAME = "price" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): String? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1] + } + + @JvmStatic + fun assemble(url: String) = arrayOf(TAG_NAME, url) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/track/tags/TypeTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/track/tags/TypeTag.kt new file mode 100644 index 000000000..fd3a227c6 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/track/tags/TypeTag.kt @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.experimental.audio.track.tags + +class TypeTag { + companion object { + const val TAG_NAME = "c" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): String? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1] + } + + @JvmStatic + fun assemble(url: String) = arrayOf(TAG_NAME, url) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/bounties/BountyAddValueEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/bounties/BountyAddValueEvent.kt new file mode 100644 index 000000000..a6ae257b0 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/bounties/BountyAddValueEvent.kt @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.experimental.bounties + +import com.vitorpamplona.quartz.nip01Core.EventHintBundle +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtag +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.nip01Core.tags.people.pTag +import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent +import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag +import com.vitorpamplona.quartz.nip10Notes.tags.markedETag +import com.vitorpamplona.quartz.nip31Alts.alt +import com.vitorpamplona.quartz.utils.TimeUtils +import java.math.BigDecimal + +/** + * This is a huge hack from back in the days... + */ +class BountyAddValueEvent { + companion object { + const val KIND = 1 + const val ALT_DESCRIPTION = "Add Value to Bounty" + + const val BOUNTY_HASH_TAG = "bounty-added-reward" + + fun build( + amount: BigDecimal, + bountyRoot: EventHintBundle, + bountyRootAuthor: PTag, + createdAt: Long = TimeUtils.now(), + ): EventTemplate { + val tags = TagArrayBuilder() + tags.markedETag(bountyRoot.toMarkedETag(MarkedETag.MARKER.ROOT)) + tags.hashtag(BOUNTY_HASH_TAG) + tags.pTag(bountyRootAuthor) + tags.alt(ALT_DESCRIPTION) + return EventTemplate(createdAt, KIND, tags.build(), amount.toString()) + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/bounties/EventExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/bounties/EventExt.kt index 94a4a603d..1b1e37669 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/bounties/EventExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/bounties/EventExt.kt @@ -21,7 +21,15 @@ package com.vitorpamplona.quartz.experimental.bounties import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.core.firstMapTagged import java.math.BigDecimal -fun Event.getReward(): BigDecimal? = tags.firstMapTagged("reward") { runCatching { BigDecimal(it[1]) }.getOrNull() } +fun Event.bountyBaseReward(): BigDecimal? = tags.bountyBaseReward() + +fun Event.hasAdditionalReward(): Boolean = tags.hasAdditionalReward() + +fun Event.addedRewardValue(): BigDecimal? = + if (hasAdditionalReward()) { + runCatching { BigDecimal(content) }.getOrNull() + } else { + null + } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/bounties/RewardTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/bounties/RewardTag.kt new file mode 100644 index 000000000..e8f3acd97 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/bounties/RewardTag.kt @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.experimental.bounties + +import java.math.BigDecimal + +class RewardTag { + companion object { + const val TAG_NAME = "reward" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): BigDecimal? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + + return runCatching { BigDecimal(tag[1]) }.getOrNull() + } + + @JvmStatic + fun assemble(amount: BigDecimal) = arrayOf(TAG_NAME, amount.toString()) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/bounties/TagArrayExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/bounties/TagArrayExt.kt new file mode 100644 index 000000000..c659d4292 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/bounties/TagArrayExt.kt @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.experimental.bounties + +import com.vitorpamplona.quartz.experimental.bounties.BountyAddValueEvent.Companion.BOUNTY_HASH_TAG +import com.vitorpamplona.quartz.nip01Core.core.TagArray +import com.vitorpamplona.quartz.nip01Core.tags.hashtags.isTaggedHash + +fun TagArray.bountyBaseReward() = this.firstNotNullOfOrNull(RewardTag::parse) + +fun TagArray.hasAdditionalReward() = this.isTaggedHash(BOUNTY_HASH_TAG) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/edits/PrivateOutboxRelayListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/edits/PrivateOutboxRelayListEvent.kt index e15f7cfa2..74b069dc4 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/edits/PrivateOutboxRelayListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/edits/PrivateOutboxRelayListEvent.kt @@ -30,7 +30,7 @@ import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip21UriScheme.toNostrUri -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer +import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.pointerSizeInBytes @@ -100,7 +100,7 @@ class PrivateOutboxRelayListEvent( companion object { const val KIND = 10013 - val TAGS = arrayOf(AltTagSerializer.toTagArray("Relay list to store private content from this author")) + val TAGS = arrayOf(AltTag.assemble("Relay list to store private content from this author")) fun createAddressATag(pubKey: HexKey): ATag = ATag(KIND, pubKey, FIXED_D_TAG, null) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/edits/TextNoteModificationEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/edits/TextNoteModificationEvent.kt index 116b01825..79dad5979 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/edits/TextNoteModificationEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/edits/TextNoteModificationEvent.kt @@ -25,7 +25,7 @@ import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.events.firstTaggedEvent -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer +import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.utils.TimeUtils @Immutable @@ -64,7 +64,7 @@ class TextNoteModificationEvent( tags.add(arrayOf("summary", it)) } - tags.add(AltTagSerializer.toTagArray(ALT)) + tags.add(AltTag.assemble(ALT)) signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/forks/BaseThreadedEventExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/forks/BaseThreadedEventExt.kt new file mode 100644 index 000000000..45c97f641 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/forks/BaseThreadedEventExt.kt @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.experimental.forks + +import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent +import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag + +fun BaseThreadedEvent.isAFork() = tags.any { it.size > 3 && (it[0] == "a" || it[0] == "e") && it[3] == "fork" } + +fun BaseThreadedEvent.forkFromAddress() = + tags.firstOrNull { it.size > 3 && it[0] == "a" && it[3] == "fork" }?.let { + val aTagValue = it[1] + val relay = it.getOrNull(2) + + ATag.parse(aTagValue, relay) + } + +fun BaseThreadedEvent.forkFromVersion() = tags.firstNotNullOfOrNull(MarkedETag::parseFork) + +fun BaseThreadedEvent.isForkFromAddressWithPubkey(authorHex: HexKey) = tags.any { it.size > 3 && it[0] == "a" && it[3] == "fork" && it[1].contains(authorHex) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/forks/MarkedETagExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/forks/MarkedETagExt.kt new file mode 100644 index 000000000..9a3217b0e --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/forks/MarkedETagExt.kt @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.experimental.forks + +import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag +import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag.MARKER + +fun MarkedETag.Companion.parseFork(tag: Array): MarkedETag? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + if (tag[ORDER_MARKER] != MARKER.FORK.code) return null + // ["e", id hex, relay hint, marker, pubkey] + return MarkedETag( + tag[ORDER_EVT_ID], + tag[ORDER_RELAY], + tag[ORDER_MARKER], + tag.getOrNull( + ORDER_PUBKEY, + ), + ) +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/inlineMetadata/Nip54InlineMetadata.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/inlineMetadata/Nip54InlineMetadata.kt index b09f6fb99..9c3822ab7 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/inlineMetadata/Nip54InlineMetadata.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/inlineMetadata/Nip54InlineMetadata.kt @@ -20,28 +20,22 @@ */ package com.vitorpamplona.quartz.experimental.inlineMetadata -import com.vitorpamplona.quartz.nip92IMeta.IMetaTag import java.net.URI import java.net.URLDecoder import java.net.URLEncoder import kotlin.coroutines.cancellation.CancellationException class Nip54InlineMetadata { - fun createUrl(header: IMetaTag): String = - createUrl( - header.url, - header.properties, - ) - fun createUrl( url: String, - tags: Map, + tags: Map>, ): String { val extension = tags .mapNotNull { - if (it.key != "url") { - "${it.key}=${URLEncoder.encode(it.value, "utf-8")}" + val value = it.value.firstOrNull() + if (it.key != "url" && value != null) { + "${it.key}=${URLEncoder.encode(value, "utf-8")}" } else { null } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/interactiveStories/InteractiveStoryBaseEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/interactiveStories/InteractiveStoryBaseEvent.kt index 7446884a4..96518fcb3 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/interactiveStories/InteractiveStoryBaseEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/interactiveStories/InteractiveStoryBaseEvent.kt @@ -20,23 +20,12 @@ */ package com.vitorpamplona.quartz.experimental.interactiveStories +import com.vitorpamplona.quartz.experimental.interactiveStories.tags.StoryOptionTag import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent -import com.vitorpamplona.quartz.nip01Core.core.firstTagValue -import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag -import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohashMipMap -import com.vitorpamplona.quartz.nip01Core.tags.hashtags.buildHashtagTags -import com.vitorpamplona.quartz.nip10Notes.content.buildUrlRefs -import com.vitorpamplona.quartz.nip10Notes.content.findHashtags -import com.vitorpamplona.quartz.nip10Notes.content.findURLs -import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrl -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer -import com.vitorpamplona.quartz.nip36SensitiveContent.ContentWarningSerializer -import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetup -import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetupSerializer -import com.vitorpamplona.quartz.nip57Zaps.zapraiser.ZapRaiserSerializer -import com.vitorpamplona.quartz.nip92IMeta.IMetaTag -import com.vitorpamplona.quartz.nip92IMeta.Nip92MediaAttachments +import com.vitorpamplona.quartz.nip23LongContent.tags.ImageTag +import com.vitorpamplona.quartz.nip23LongContent.tags.SummaryTag +import com.vitorpamplona.quartz.nip23LongContent.tags.TitleTag open class InteractiveStoryBaseEvent( id: HexKey, @@ -47,75 +36,11 @@ open class InteractiveStoryBaseEvent( content: String, sig: HexKey, ) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) { - fun title() = tags.firstTagValue("title") + fun title() = tags.firstNotNullOfOrNull(TitleTag::parse) - fun summary() = tags.firstTagValue("summary") + fun summary() = tags.firstNotNullOfOrNull(SummaryTag::parse) - fun image() = tags.firstTagValue("image") + fun image() = tags.firstNotNullOfOrNull(ImageTag::parse) - fun options() = - tags - .filter { it.size > 2 && it[0] == "option" } - .mapNotNull { ATag.parse(it[2], it.getOrNull(3))?.let { aTag -> StoryOption(it[1], aTag) } } - - companion object { - fun generalTags( - content: String, - zapReceiver: List? = null, - markAsSensitive: Boolean = false, - zapRaiserAmount: Long? = null, - geohash: String? = null, - imetas: List? = null, - emojis: List? = null, - ): Array> { - val tags = mutableListOf>() - - tags.addAll(buildHashtagTags(findHashtags(content))) - tags.addAll(buildUrlRefs(findURLs(content))) - zapReceiver?.forEach { tags.add(ZapSplitSetupSerializer.toTagArray(it)) } - zapRaiserAmount?.let { tags.add(ZapRaiserSerializer.toTagArray(it)) } - - if (markAsSensitive) { - tags.add(ContentWarningSerializer.toTagArray()) - } - - geohash?.let { tags.addAll(geohashMipMap(it)) } - imetas?.forEach { - tags.add(Nip92MediaAttachments.createTag(it)) - } - emojis?.forEach { tags.add(it.toTagArray()) } - return tags.toTypedArray() - } - - fun makeTags( - baseId: String, - alt: String, - title: String, - summary: String? = null, - image: String? = null, - options: List = emptyList(), - ): Array> = - ( - listOfNotNull( - arrayOf("d", baseId), - arrayOf("title", title), - summary?.let { arrayOf("summary", it) }, - image?.let { arrayOf("image", it) }, - AltTagSerializer.toTagArray(alt), - ) + - options.map { - val relayUrl = it.address.relay - if (relayUrl != null) { - arrayOf("option", it.option, it.address.toTag(), relayUrl) - } else { - arrayOf("option", it.option, it.address.toTag()) - } - } - ).toTypedArray() - } + fun options() = tags.mapNotNull(StoryOptionTag::parse) } - -class StoryOption( - val option: String, - val address: ATag, -) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/interactiveStories/InteractiveStoryPrologueEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/interactiveStories/InteractiveStoryPrologueEvent.kt index 3cbffc1cb..df6f3d15a 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/interactiveStories/InteractiveStoryPrologueEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/interactiveStories/InteractiveStoryPrologueEvent.kt @@ -20,12 +20,14 @@ */ package com.vitorpamplona.quartz.experimental.interactiveStories +import com.vitorpamplona.quartz.experimental.interactiveStories.tags.StoryOptionTag import com.vitorpamplona.quartz.nip01Core.HexKey -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag import com.vitorpamplona.quartz.nip22Comments.RootScope -import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetup -import com.vitorpamplona.quartz.nip92IMeta.IMetaTag import com.vitorpamplona.quartz.utils.TimeUtils class InteractiveStoryPrologueEvent( @@ -39,7 +41,7 @@ class InteractiveStoryPrologueEvent( RootScope { companion object { const val KIND = 30296 - const val ALT = "The prologue of an interative story called " + const val ALT = "The prologue of an interactive story called " fun createAddressATag( pubKey: HexKey, @@ -51,32 +53,19 @@ class InteractiveStoryPrologueEvent( dtag: String, ): String = ATag.assembleATagId(KIND, pubKey, dtag) - fun create( + fun build( baseId: String, title: String, content: String, - options: List, - summary: String? = null, - image: String? = null, - zapReceiver: List? = null, - markAsSensitive: Boolean = false, - zapRaiserAmount: Long? = null, - geohash: String? = null, - imetas: List? = null, - signer: NostrSigner, + options: List, createdAt: Long = TimeUtils.now(), - isDraft: Boolean, - onReady: (InteractiveStoryPrologueEvent) -> Unit, - ) { - val tags = - makeTags(baseId, ALT + title, title, summary, image, options) + - generalTags(content, zapReceiver, markAsSensitive, zapRaiserAmount, geohash, imetas) - - if (isDraft) { - signer.assembleRumor(createdAt, KIND, tags, content, onReady) - } else { - signer.sign(createdAt, KIND, tags, content, onReady) + initializer: TagArrayBuilder.() -> Unit = {}, + ): EventTemplate = + eventTemplate(KIND, content, createdAt) { + dTag(baseId) + title(title) + options(options) + initializer() } - } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/interactiveStories/InteractiveStoryReadingStateEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/interactiveStories/InteractiveStoryReadingStateEvent.kt index 1002f5ca7..c81c9ab4e 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/interactiveStories/InteractiveStoryReadingStateEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/interactiveStories/InteractiveStoryReadingStateEvent.kt @@ -21,15 +21,22 @@ package com.vitorpamplona.quartz.experimental.interactiveStories import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.experimental.interactiveStories.tags.ReadStatusTag +import com.vitorpamplona.quartz.experimental.interactiveStories.tags.RootSceneTag import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent -import com.vitorpamplona.quartz.nip01Core.core.firstTag -import com.vitorpamplona.quartz.nip01Core.core.firstTagValue -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.core.builder +import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer +import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag +import com.vitorpamplona.quartz.nip23LongContent.tags.ImageTag +import com.vitorpamplona.quartz.nip23LongContent.tags.TitleTag +import com.vitorpamplona.quartz.nip31Alts.alt +import com.vitorpamplona.quartz.nip94FileMetadata.tags.SummaryTag +import com.vitorpamplona.quartz.nip99Classifieds.tags.StatusTag import com.vitorpamplona.quartz.utils.TimeUtils -import com.vitorpamplona.quartz.utils.removeTrailingNullsAndEmptyOthers @Immutable class InteractiveStoryReadingStateEvent( @@ -40,17 +47,17 @@ class InteractiveStoryReadingStateEvent( content: String, sig: HexKey, ) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) { - fun title() = tags.firstTagValue("title") + fun title() = tags.firstNotNullOfOrNull(TitleTag::parse) - fun summary() = tags.firstTagValue("summary") + fun summary() = tags.firstNotNullOfOrNull(SummaryTag::parse) - fun image() = tags.firstTagValue("image") + fun image() = tags.firstNotNullOfOrNull(ImageTag::parse) - fun status() = tags.firstTagValue("status") + fun status() = tags.firstNotNullOfOrNull(StatusTag::parse) - fun root() = tags.firstTag("A")?.let { ATag.parse(it[1], it.getOrNull(2)) } + fun root() = tags.firstNotNullOfOrNull(RootSceneTag::parse) - fun currentScene() = tags.firstTag("a")?.let { ATag.parse(it[1], it.getOrNull(2)) } + fun currentScene() = tags.firstNotNullOfOrNull(ATag::parse) companion object { const val KIND = 30298 @@ -71,71 +78,60 @@ class InteractiveStoryReadingStateEvent( base: InteractiveStoryReadingStateEvent, currentScene: InteractiveStoryBaseEvent, currentSceneRelay: String?, - signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (InteractiveStoryReadingStateEvent) -> Unit, - ) { + ): EventTemplate { val rootTag = base.dTag() - val sceneTag = currentScene.addressTag() + val sceneTag = currentScene.aTag(currentSceneRelay) val status = - if (rootTag == sceneTag) { - "new" + if (rootTag == sceneTag.toTag()) { + ReadStatusTag.STATUS.NEW } else if (currentScene.options().isEmpty()) { - "done" + ReadStatusTag.STATUS.DONE } else { - "reading" + ReadStatusTag.STATUS.READING } - val tags = - base.tags.filter { it[0] != "a" && it[0] != "status" } + - listOf( - removeTrailingNullsAndEmptyOthers("a", sceneTag, currentSceneRelay), - arrayOf("status", status), - ) + val updatedTags = + base.tags.builder { + currentScene(sceneTag) + status(status) + } - signer.sign(createdAt, KIND, tags.toTypedArray(), "", onReady) + return EventTemplate(createdAt, KIND, updatedTags, "") } - fun create( + fun build( root: InteractiveStoryBaseEvent, rootRelay: String?, currentScene: InteractiveStoryBaseEvent, currentSceneRelay: String?, - signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (InteractiveStoryReadingStateEvent) -> Unit, - ) { - val rootTag = root.addressTag() - val sceneTag = currentScene.addressTag() + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, "", createdAt) { + val rootTag = root.aTag(rootRelay) + val sceneTag = currentScene.aTag(currentSceneRelay) val status = if (rootTag == sceneTag) { - "new" + ReadStatusTag.STATUS.NEW } else if (currentScene.options().isEmpty()) { - "done" + ReadStatusTag.STATUS.DONE } else { - "reading" + ReadStatusTag.STATUS.READING } - val tags = - listOfNotNull( - arrayOf("d", rootTag), - AltTagSerializer.toTagArray(root.title()?.let { ALT2 + it } ?: ALT1), - root.title()?.let { arrayOf("title", it) }, - root.summary()?.let { arrayOf("summary", it) }, - root.image()?.let { arrayOf("image", it) }, - removeTrailingNullsAndEmptyOthers("A", rootTag, rootRelay), - removeTrailingNullsAndEmptyOthers("a", sceneTag, currentSceneRelay), - arrayOf("status", status), - ).toTypedArray() + dTag(rootTag.toTag()) + alt(root.title()?.let { ALT2 + it } ?: ALT1) - signer.sign(createdAt, KIND, tags, "", onReady) + rootScene(rootTag) + currentScene(sceneTag) + status(status) + + root.title()?.let { storyTitle(it) } + root.summary()?.let { storyImage(it) } + root.image()?.let { storySummary(it) } + + initializer() } } - - enum class ReadingStatus { - NEW, - READING, - DONE, - } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/interactiveStories/InteractiveStorySceneEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/interactiveStories/InteractiveStorySceneEvent.kt index 3b2b46d9f..4b3a8cff8 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/interactiveStories/InteractiveStorySceneEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/interactiveStories/InteractiveStorySceneEvent.kt @@ -20,12 +20,13 @@ */ package com.vitorpamplona.quartz.experimental.interactiveStories +import com.vitorpamplona.quartz.experimental.interactiveStories.tags.StoryOptionTag import com.vitorpamplona.quartz.nip01Core.HexKey -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag import com.vitorpamplona.quartz.nip22Comments.RootScope -import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetup -import com.vitorpamplona.quartz.nip92IMeta.IMetaTag import com.vitorpamplona.quartz.utils.TimeUtils class InteractiveStorySceneEvent( @@ -51,30 +52,18 @@ class InteractiveStorySceneEvent( dtag: String, ): String = ATag.assembleATagId(KIND, pubKey, dtag) - fun create( + fun build( baseId: String, title: String, content: String, - options: List, - zapReceiver: List? = null, - markAsSensitive: Boolean = false, - zapRaiserAmount: Long? = null, - geohash: String? = null, - imetas: List? = null, - signer: NostrSigner, + options: List, createdAt: Long = TimeUtils.now(), - isDraft: Boolean, - onReady: (InteractiveStorySceneEvent) -> Unit, - ) { - val tags = - makeTags(baseId, ALT + title, title, options = options) + - generalTags(content, zapReceiver, markAsSensitive, zapRaiserAmount, geohash, imetas) - - if (isDraft) { - signer.assembleRumor(createdAt, KIND, tags, content, onReady) - } else { - signer.sign(createdAt, KIND, tags, content, onReady) - } + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, content, createdAt) { + dTag(baseId) + title(title) + options(options) + initializer() } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/interactiveStories/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/interactiveStories/TagArrayBuilderExt.kt new file mode 100644 index 000000000..002868786 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/interactiveStories/TagArrayBuilderExt.kt @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.experimental.interactiveStories + +import com.vitorpamplona.quartz.experimental.interactiveStories.tags.ReadStatusTag +import com.vitorpamplona.quartz.experimental.interactiveStories.tags.StoryOptionTag +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip23LongContent.tags.ImageTag +import com.vitorpamplona.quartz.nip23LongContent.tags.PublishedAtTag +import com.vitorpamplona.quartz.nip23LongContent.tags.SummaryTag +import com.vitorpamplona.quartz.nip23LongContent.tags.TitleTag + +fun TagArrayBuilder.title(title: String) = addUnique(TitleTag.assemble(title)) + +fun TagArrayBuilder.option(option: StoryOptionTag) = add(option.toTagArray()) + +fun TagArrayBuilder.options(options: List) = addAll(options.map { it.toTagArray() }) + +fun TagArrayBuilder.publishedAt(publishedAt: Long) = addUnique(PublishedAtTag.assemble(publishedAt)) + +fun TagArrayBuilder.summary(summary: String) = addUnique(SummaryTag.assemble(summary)) + +fun TagArrayBuilder.image(imageUrl: String) = add(ImageTag.assemble(imageUrl)) + +fun TagArrayBuilder.images(imageUrls: List) = addAll(imageUrls.map { ImageTag.assemble(it) }) + +fun TagArrayBuilder.storyTitle(title: String) = addUnique(TitleTag.assemble(title)) + +fun TagArrayBuilder.storySummary(summary: String) = addUnique(SummaryTag.assemble(summary)) + +fun TagArrayBuilder.storyImage(imageUrl: String) = add(ImageTag.assemble(imageUrl)) + +fun TagArrayBuilder.storyImages(imageUrls: List) = addAll(imageUrls.map { ImageTag.assemble(it) }) + +fun TagArrayBuilder.rootScene(scene: ATag) = addUnique(scene.toATagArray()) + +fun TagArrayBuilder.currentScene(scene: ATag) = addUnique(scene.toATagArray()) + +fun TagArrayBuilder.status(status: ReadStatusTag.STATUS) = addUnique(status.toTagArray()) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/interactiveStories/tags/ReadStatusTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/interactiveStories/tags/ReadStatusTag.kt new file mode 100644 index 000000000..c09d4e768 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/interactiveStories/tags/ReadStatusTag.kt @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.experimental.interactiveStories.tags + +class ReadStatusTag { + enum class STATUS( + val value: String, + ) { + NEW("new"), + READING("reading"), + DONE("done"), + ; + + fun toTagArray() = assemble(this) + } + + companion object { + const val TAG_NAME = "status" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): String? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1] + } + + @JvmStatic + fun assemble(status: STATUS) = arrayOf(TAG_NAME, status.value) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/interactiveStories/tags/RootSceneTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/interactiveStories/tags/RootSceneTag.kt new file mode 100644 index 000000000..6a14f8e78 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/interactiveStories/tags/RootSceneTag.kt @@ -0,0 +1,94 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.experimental.interactiveStories.tags + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address +import com.vitorpamplona.quartz.utils.arrayOfNotNull +import com.vitorpamplona.quartz.utils.bytesUsedInMemory +import com.vitorpamplona.quartz.utils.pointerSizeInBytes +import com.vitorpamplona.quartz.utils.removeTrailingNullsAndEmptyOthers + +@Immutable +data class RootSceneTag( + val kind: Int, + val pubKeyHex: String, + val dTag: String, +) { + var relay: String? = null + + constructor( + kind: Int, + pubKeyHex: HexKey, + dTag: String, + relayHint: String?, + ) : this(kind, pubKeyHex, dTag) { + this.relay = relayHint + } + + fun countMemory(): Long = + 5 * pointerSizeInBytes + // 7 fields, 4 bytes each reference (32bit) + 8L + // kind + pubKeyHex.bytesUsedInMemory() + + dTag.bytesUsedInMemory() + + (relay?.bytesUsedInMemory() ?: 0) + + fun toTag() = assembleATagId(kind, pubKeyHex, dTag) + + fun toTagArray() = removeTrailingNullsAndEmptyOthers(TAG_NAME, toTag(), relay) + + companion object { + const val TAG_NAME = "A" + const val TAG_SIZE = 2 + + fun assembleATagId( + kind: Int, + pubKeyHex: HexKey, + dTag: String, + ) = Address.assemble(kind, pubKeyHex, dTag) + + fun parse( + aTagId: String, + relay: String?, + ) = Address.parse(aTagId)?.let { RootSceneTag(it.kind, it.pubKeyHex, it.dTag, relay) } + + @JvmStatic + fun parse(tag: Array): RootSceneTag? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return parse(tag[1], tag.getOrNull(2)) + } + + @JvmStatic + fun assemble( + aTagId: HexKey, + relay: String?, + ) = arrayOfNotNull(TAG_NAME, aTagId, relay) + + @JvmStatic + fun assemble( + kind: Int, + pubKeyHex: String, + dTag: String, + relay: String?, + ) = arrayOfNotNull(TAG_NAME, assembleATagId(kind, pubKeyHex, dTag), relay) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/interactiveStories/tags/StoryOptionTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/interactiveStories/tags/StoryOptionTag.kt new file mode 100644 index 000000000..b4a5fabc9 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/interactiveStories/tags/StoryOptionTag.kt @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.experimental.interactiveStories.tags + +import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.utils.arrayOfNotNull + +class StoryOptionTag( + val option: String, + val address: ATag, +) { + fun toTagArray() = assemble(option, address) + + companion object { + const val TAG_NAME = "option" + const val TAG_SIZE = 3 + + @JvmStatic + fun parse(tag: Array): StoryOptionTag? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + + return ATag.parse(tag[2], tag.getOrNull(3))?.let { StoryOptionTag(tag[1], it) } + } + + @JvmStatic + fun assemble( + title: String, + destination: ATag, + ) = arrayOfNotNull(TAG_NAME, title, destination.toTag(), destination.relay) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/limits/LimitProcessor.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/limits/LimitProcessor.kt index efe076e09..f3c829d9f 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/limits/LimitProcessor.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/limits/LimitProcessor.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.quartz.experimental.limits import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.relays.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip13Pow.pow import com.vitorpamplona.quartz.utils.TimeUtils diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/medical/FhirResourceEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/medical/FhirResourceEvent.kt index abac11259..c03618f33 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/medical/FhirResourceEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/medical/FhirResourceEvent.kt @@ -21,9 +21,12 @@ package com.vitorpamplona.quartz.experimental.medical import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.experimental.nip95.data.FileStorageEvent.Companion.ALT import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.utils.TimeUtils @Immutable @@ -37,16 +40,15 @@ class FhirResourceEvent( ) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { companion object { const val KIND = 82 + const val ALT_DESCRIPTION = "Medical data" - fun create( + fun build( fhirPayload: String, - signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (FhirResourceEvent) -> Unit, - ) { - val tags = mutableListOf>() - - signer.sign(createdAt, KIND, tags.toTypedArray(), fhirPayload, onReady) + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, fhirPayload, createdAt) { + alt(ALT) + initializer() } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/nip95/FileStorageHeaderEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/nip95/FileStorageHeaderEvent.kt deleted file mode 100644 index b21bdd6f8..000000000 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/nip95/FileStorageHeaderEvent.kt +++ /dev/null @@ -1,115 +0,0 @@ -/** - * Copyright (c) 2024 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.quartz.experimental.nip95 - -import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey -import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer -import com.vitorpamplona.quartz.nip36SensitiveContent.ContentWarningSerializer -import com.vitorpamplona.quartz.nip94FileMetadata.Dimension -import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent -import com.vitorpamplona.quartz.utils.TimeUtils - -@Immutable -class FileStorageHeaderEvent( - id: HexKey, - pubKey: HexKey, - createdAt: Long, - tags: Array>, - content: String, - sig: HexKey, -) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { - fun dataEventId() = tags.firstOrNull { it.size > 1 && it[0] == "e" }?.get(1) - - fun mimeType() = tags.firstOrNull { it.size > 1 && it[0] == MIME_TYPE }?.get(1) - - fun hash() = tags.firstOrNull { it.size > 1 && it[0] == HASH }?.get(1) - - fun size() = tags.firstOrNull { it.size > 1 && it[0] == FILE_SIZE }?.get(1) - - fun alt() = tags.firstOrNull { it.size > 1 && it[0] == ALT }?.get(1) - - fun dimensions() = tags.firstOrNull { it.size > 1 && it[0] == DIMENSION }?.get(1)?.let { Dimension.parse(it) } - - fun magnetURI() = tags.firstOrNull { it.size > 1 && it[0] == MAGNET_URI }?.get(1) - - fun torrentInfoHash() = tags.firstOrNull { it.size > 1 && it[0] == TORRENT_INFOHASH }?.get(1) - - fun blurhash() = tags.firstOrNull { it.size > 1 && it[0] == BLUR_HASH }?.get(1) - - fun isOneOf(mimeTypes: Set) = tags.any { it.size > 1 && it[0] == FileHeaderEvent.MIME_TYPE && mimeTypes.contains(it[1]) } - - companion object { - const val KIND = 1065 - const val ALT_DESCRIPTION = "Descriptors for a binary file" - - private const val ENCRYPTION_KEY = "aes-256-gcm" - private const val MIME_TYPE = "m" - private const val FILE_SIZE = "size" - private const val DIMENSION = "dim" - private const val HASH = "x" - private const val MAGNET_URI = "magnet" - private const val TORRENT_INFOHASH = "i" - private const val BLUR_HASH = "blurhash" - private const val ALT = "alt" - - fun create( - storageEvent: FileStorageEvent, - mimeType: String? = null, - alt: String? = null, - hash: String? = null, - size: String? = null, - dimensions: Dimension? = null, - blurhash: String? = null, - magnetURI: String? = null, - torrentInfoHash: String? = null, - sensitiveContent: Boolean? = null, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (FileStorageHeaderEvent) -> Unit, - ) { - val tags = - listOfNotNull( - arrayOf("e", storageEvent.id), - mimeType?.let { arrayOf(MIME_TYPE, mimeType) }, - hash?.let { arrayOf(HASH, it) }, - alt?.let { arrayOf(ALT, it) } ?: AltTagSerializer.toTagArray(ALT_DESCRIPTION), - size?.let { arrayOf(FILE_SIZE, it) }, - dimensions?.let { arrayOf(DIMENSION, it.toString()) }, - blurhash?.let { arrayOf(BLUR_HASH, it) }, - magnetURI?.let { arrayOf(MAGNET_URI, it) }, - torrentInfoHash?.let { arrayOf(TORRENT_INFOHASH, it) }, - sensitiveContent?.let { - if (it) { - ContentWarningSerializer.toTagArray() - } else { - null - } - }, - ) - - val content = alt ?: "" - signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady) - } - } -} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/nip95/FileStorageEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/nip95/data/FileStorageEvent.kt similarity index 64% rename from quartz/src/main/java/com/vitorpamplona/quartz/experimental/nip95/FileStorageEvent.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/experimental/nip95/data/FileStorageEvent.kt index 9bcb44ad0..01af75249 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/nip95/FileStorageEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/nip95/data/FileStorageEvent.kt @@ -18,14 +18,15 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.experimental.nip95 +package com.vitorpamplona.quartz.experimental.nip95.data -import android.util.Log import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.utils.TimeUtils import java.util.Base64 @@ -40,40 +41,26 @@ class FileStorageEvent( ) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { override fun isContentEncoded() = true - fun type() = tags.firstOrNull { it.size > 1 && it[0] == TYPE }?.get(1) - - fun decode(): ByteArray? = - try { - Base64.getDecoder().decode(content) - } catch (e: Exception) { - Log.e("FileStorageEvent", "Unable to decode base 64 ${e.message} $content") - null - } + fun decode(): ByteArray? = decode(content) companion object { const val KIND = 1064 const val ALT = "Binary data" - private const val TYPE = "type" - private const val DECRYPT = "decrypt" + fun decode(content: String): ByteArray? = runCatching { Base64.getDecoder().decode(content) }.getOrNull() fun encode(bytes: ByteArray): String = Base64.getEncoder().encodeToString(bytes) - fun create( - mimeType: String, + fun build( data: ByteArray, - signer: NostrSigner, + mimeType: String? = null, createdAt: Long = TimeUtils.now(), - onReady: (FileStorageEvent) -> Unit, - ) { - val tags = - listOfNotNull( - arrayOf(TYPE, mimeType), - AltTagSerializer.toTagArray(ALT), - ) - - val content = encode(data) - signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady) - } + initializer: TagArrayBuilder.() -> Unit = {}, + ): EventTemplate = + eventTemplate(KIND, encode(data), createdAt) { + alt(ALT) + mimeType?.let { mimeType(it) } + initializer() + } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/nip95/data/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/nip95/data/TagArrayBuilderExt.kt new file mode 100644 index 000000000..d7f082826 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/nip95/data/TagArrayBuilderExt.kt @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.experimental.nip95.data + +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip94FileMetadata.tags.MimeTypeTag + +fun TagArrayBuilder.mimeType(mimeType: String) = add(MimeTypeTag.assemble(mimeType)) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/nip95/header/FileStorageHeaderEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/nip95/header/FileStorageHeaderEvent.kt new file mode 100644 index 000000000..9b8a5b71a --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/nip95/header/FileStorageHeaderEvent.kt @@ -0,0 +1,104 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.experimental.nip95.header + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.experimental.nip95.data.FileStorageEvent +import com.vitorpamplona.quartz.nip01Core.EventHintBundle +import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.core.any +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag +import com.vitorpamplona.quartz.nip01Core.tags.events.eTag +import com.vitorpamplona.quartz.nip31Alts.alt +import com.vitorpamplona.quartz.nip94FileMetadata.tags.BlurhashTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.FallbackTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.HashTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.ImageTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.MagnetTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.MimeTypeTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.ServiceTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.SizeTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.SummaryTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.ThumbTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.TorrentInfoHash +import com.vitorpamplona.quartz.utils.TimeUtils + +@Immutable +class FileStorageHeaderEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { + fun dataEvent() = tags.mapNotNull(ETag::parse) + + fun dataEventIds() = tags.mapNotNull(ETag::parseId) + + fun dataEventId() = tags.firstNotNullOfOrNull(ETag::parseId) + + fun mimeType() = tags.firstNotNullOfOrNull(MimeTypeTag::parse) + + fun hash() = tags.firstNotNullOfOrNull(HashTag::parse) + + fun size() = tags.firstNotNullOfOrNull(SizeTag::parse) + + fun dimensions() = tags.firstNotNullOfOrNull(DimensionTag::parse) + + fun magnetURI() = tags.firstNotNullOfOrNull(MagnetTag::parse) + + fun torrentInfoHash() = tags.firstNotNullOfOrNull(TorrentInfoHash::parse) + + fun blurhash() = tags.firstNotNullOfOrNull(BlurhashTag::parse) + + fun image() = tags.firstNotNullOfOrNull(ImageTag::parse) + + fun thumb() = tags.firstNotNullOfOrNull(ThumbTag::parse) + + fun service() = tags.firstNotNullOfOrNull(ServiceTag::parse) + + fun summary() = tags.firstNotNullOfOrNull(SummaryTag::parse) + + fun fallback() = tags.firstNotNullOfOrNull(FallbackTag::parse) + + fun isOneOf(mimeTypes: Set) = tags.any(MimeTypeTag::isIn, mimeTypes) + + companion object { + const val KIND = 1065 + const val ALT_DESCRIPTION = "Descriptors for a binary file" + + fun build( + storageEvent: EventHintBundle, + caption: String?, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, caption ?: "", createdAt) { + eTag(storageEvent.toETag()) + caption?.ifBlank { null }?.let { alt(caption) } ?: alt(ALT_DESCRIPTION) + initializer() + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/nip95/header/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/nip95/header/TagArrayBuilderExt.kt new file mode 100644 index 000000000..69911a409 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/nip95/header/TagArrayBuilderExt.kt @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.experimental.nip95.header + +import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip94FileMetadata.tags.BlurhashTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.FallbackTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.HashTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.ImageTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.MagnetTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.MimeTypeTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.ServiceTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.SizeTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.SummaryTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.ThumbTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.TorrentInfoHash + +fun TagArrayBuilder.mimeType(mimeType: String) = add(MimeTypeTag.assemble(mimeType)) + +fun TagArrayBuilder.hash(hash: HexKey) = add(HashTag.assemble(hash)) + +fun TagArrayBuilder.fileSize(size: Int) = add(SizeTag.assemble(size)) + +fun TagArrayBuilder.dimension(dim: DimensionTag) = add(DimensionTag.assemble(dim)) + +fun TagArrayBuilder.blurhash(blurhash: String) = add(BlurhashTag.assemble(blurhash)) + +fun TagArrayBuilder.torrentInfohash(hash: String) = add(TorrentInfoHash.assemble(hash)) + +fun TagArrayBuilder.magnet(magnetUri: String) = add(MagnetTag.assemble(magnetUri)) + +fun TagArrayBuilder.image(imageUrl: HexKey) = add(ImageTag.assemble(imageUrl)) + +fun TagArrayBuilder.thumb(trumbUrl: HexKey) = add(ThumbTag.assemble(trumbUrl)) + +fun TagArrayBuilder.summary(summary: HexKey) = add(SummaryTag.assemble(summary)) + +fun TagArrayBuilder.fallback(fallbackUrl: HexKey) = add(FallbackTag.assemble(fallbackUrl)) + +fun TagArrayBuilder.service(service: HexKey) = add(ServiceTag.assemble(service)) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/nns/NNSEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/nns/NNSEvent.kt index dac617f41..1fc8486f6 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/nns/NNSEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/nns/NNSEvent.kt @@ -21,10 +21,14 @@ package com.vitorpamplona.quartz.experimental.nns import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.experimental.nns.tags.IPv4Tag +import com.vitorpamplona.quartz.experimental.nns.tags.IPv6Tag +import com.vitorpamplona.quartz.experimental.nns.tags.VersionTag import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.utils.TimeUtils @Immutable @@ -36,23 +40,32 @@ class NNSEvent( content: String, sig: HexKey, ) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) { - fun ip4() = tags.firstOrNull { it.size > 1 && it[0] == "ip4" }?.get(1) + fun ip4() = tags.firstNotNullOfOrNull(IPv4Tag::parse) - fun ip6() = tags.firstOrNull { it.size > 1 && it[0] == "ip6" }?.get(1) + fun ip6() = tags.firstNotNullOfOrNull(IPv6Tag::parse) - fun version() = tags.firstOrNull { it.size > 1 && it[0] == "version" }?.get(1) + fun version() = tags.firstNotNullOfOrNull(VersionTag::parse) companion object { const val KIND = 30053 const val ALT = "DNS records" - fun create( - signer: NostrSigner, + fun build( createdAt: Long = TimeUtils.now(), - onReady: (NNSEvent) -> Unit, - ) { - val tags = arrayOf(AltTagSerializer.toTagArray(ALT)) - signer.sign(createdAt, KIND, tags, "", onReady) + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, "", createdAt) { + alt(ALT) + initializer() + } + + fun build( + ipv4: String, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, "", createdAt) { + alt(ALT) + ipv4(ipv4) + initializer() } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/nns/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/nns/TagArrayBuilderExt.kt new file mode 100644 index 000000000..6411d24bb --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/nns/TagArrayBuilderExt.kt @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.experimental.nns + +import com.vitorpamplona.quartz.experimental.nns.tags.IPv4Tag +import com.vitorpamplona.quartz.experimental.nns.tags.IPv6Tag +import com.vitorpamplona.quartz.experimental.nns.tags.VersionTag +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder + +fun TagArrayBuilder.ipv4(ip: String) = add(IPv4Tag.assemble(ip)) + +fun TagArrayBuilder.ipv6(ip: String) = add(IPv6Tag.assemble(ip)) + +fun TagArrayBuilder.version(version: String) = add(VersionTag.assemble(version)) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/nns/tags/IPv4Tag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/nns/tags/IPv4Tag.kt new file mode 100644 index 000000000..c8f7ded13 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/nns/tags/IPv4Tag.kt @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.experimental.nns.tags + +class IPv4Tag { + companion object { + const val TAG_NAME = "ip4" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): String? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1] + } + + @JvmStatic + fun assemble(ip: String) = arrayOf(TAG_NAME, ip) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/nns/tags/IPv6Tag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/nns/tags/IPv6Tag.kt new file mode 100644 index 000000000..07086b274 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/nns/tags/IPv6Tag.kt @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.experimental.nns.tags + +class IPv6Tag { + companion object { + const val TAG_NAME = "ip6" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): String? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1] + } + + @JvmStatic + fun assemble(ip: String) = arrayOf(TAG_NAME, ip) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/nns/tags/VersionTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/nns/tags/VersionTag.kt new file mode 100644 index 000000000..2eca2f76c --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/nns/tags/VersionTag.kt @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.experimental.nns.tags + +class VersionTag { + companion object { + const val TAG_NAME = "version" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): String? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1] + } + + @JvmStatic + fun assemble(ip: String) = arrayOf(TAG_NAME, ip) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/profileGallery/GalleryListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/profileGallery/GalleryListEvent.kt index 69a8c389b..6eace1de7 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/profileGallery/GalleryListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/profileGallery/GalleryListEvent.kt @@ -24,7 +24,7 @@ import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer +import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.nip51Lists.GeneralListEvent import com.vitorpamplona.quartz.utils.TimeUtils @@ -142,7 +142,7 @@ class GalleryListEvent( if (tags.any { it.size > 1 && it[0] == "alt" }) { tags } else { - tags + AltTagSerializer.toTagArray(ALT) + tags + AltTag.assemble(ALT) } signer.sign(createdAt, KIND, newTags, content, onReady) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/profileGallery/ProfileGalleryEntryEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/profileGallery/ProfileGalleryEntryEvent.kt index f0f36824a..aa9725a8f 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/profileGallery/ProfileGalleryEntryEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/profileGallery/ProfileGalleryEntryEvent.kt @@ -23,10 +23,24 @@ package com.vitorpamplona.quartz.experimental.profileGallery import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer -import com.vitorpamplona.quartz.nip36SensitiveContent.ContentWarningSerializer -import com.vitorpamplona.quartz.nip94FileMetadata.Dimension +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.core.any +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag +import com.vitorpamplona.quartz.nip31Alts.alt +import com.vitorpamplona.quartz.nip94FileMetadata.tags.BlurhashTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.FallbackTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.HashTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.ImageTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.MagnetTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.MimeTypeTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.ServiceTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.SizeTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.SummaryTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.ThumbTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.TorrentInfoHash +import com.vitorpamplona.quartz.nip94FileMetadata.tags.UrlTag import com.vitorpamplona.quartz.utils.TimeUtils @Immutable @@ -38,97 +52,54 @@ class ProfileGalleryEntryEvent( content: String, sig: HexKey, ) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { - fun url() = tags.firstOrNull { it.size > 1 && it[0] == URL }?.get(1) + fun url() = tags.firstNotNullOfOrNull(UrlTag::parse) - fun urls() = tags.filter { it.size > 1 && it[0] == URL }.map { it[1] } + fun urls() = tags.mapNotNull(UrlTag::parse) - fun mimeType() = tags.firstOrNull { it.size > 1 && it[0] == MIME_TYPE }?.get(1) + fun mimeType() = tags.firstNotNullOfOrNull(MimeTypeTag::parse) - fun hash() = tags.firstOrNull { it.size > 1 && it[0] == HASH }?.get(1) + fun hash() = tags.firstNotNullOfOrNull(HashTag::parse) - fun size() = tags.firstOrNull { it.size > 1 && it[0] == FILE_SIZE }?.get(1) + fun size() = tags.firstNotNullOfOrNull(SizeTag::parse) - fun alt() = tags.firstOrNull { it.size > 1 && it[0] == ALT }?.get(1) + fun dimensions() = tags.firstNotNullOfOrNull(DimensionTag::parse) - fun dimensions() = tags.firstOrNull { it.size > 1 && it[0] == DIMENSION }?.get(1)?.let { Dimension.parse(it) } + fun magnetURI() = tags.firstNotNullOfOrNull(MagnetTag::parse) - fun magnetURI() = tags.firstOrNull { it.size > 1 && it[0] == MAGNET_URI }?.get(1) + fun torrentInfoHash() = tags.firstNotNullOfOrNull(TorrentInfoHash::parse) - fun torrentInfoHash() = tags.firstOrNull { it.size > 1 && it[0] == TORRENT_INFOHASH }?.get(1) + fun blurhash() = tags.firstNotNullOfOrNull(BlurhashTag::parse) - fun blurhash() = tags.firstOrNull { it.size > 1 && it[0] == BLUR_HASH }?.get(1) + fun image() = tags.firstNotNullOfOrNull(ImageTag::parse) - fun hasUrl() = tags.any { it.size > 1 && it[0] == URL } + fun thumb() = tags.firstNotNullOfOrNull(ThumbTag::parse) - fun fromEvent() = tags.firstOrNull { it.size > 1 && it[0] == "e" }?.get(1) + fun service() = tags.firstNotNullOfOrNull(ServiceTag::parse) - fun hasFromEvent() = tags.any { it.size > 1 && it[0] == "e" } + fun summary() = tags.firstNotNullOfOrNull(SummaryTag::parse) - fun isOneOf(mimeTypes: Set) = tags.any { it.size > 1 && it[0] == MIME_TYPE && mimeTypes.contains(it[1]) } + fun fallback() = tags.firstNotNullOfOrNull(FallbackTag::parse) + + fun hasUrl() = tags.any(UrlTag::isTag) + + fun isOneOf(mimeTypes: Set) = tags.any(MimeTypeTag::isIn, mimeTypes) + + fun fromEvent() = tags.firstNotNullOfOrNull(ETag::parseId) + + fun hasFromEvent() = tags.any(ETag::isTagged) companion object { const val KIND = 1163 const val ALT_DESCRIPTION = "Profile Gallery Entry" - const val URL = "url" - const val ENCRYPTION_KEY = "aes-256-gcm" - const val MIME_TYPE = "m" - const val FILE_SIZE = "size" - const val DIMENSION = "dim" - const val HASH = "x" - const val MAGNET_URI = "magnet" - const val TORRENT_INFOHASH = "i" - const val BLUR_HASH = "blurhash" - const val ORIGINAL_HASH = "ox" - const val ALT = "alt" - - fun create( + fun build( url: String, - eventid: String? = null, - relayhint: String? = null, - magnetUri: String? = null, - mimeType: String? = null, - alt: String? = null, - hash: String? = null, - size: String? = null, - dimensions: Dimension? = null, - blurhash: String? = null, - originalHash: String? = null, - magnetURI: String? = null, - torrentInfoHash: String? = null, - sensitiveContent: Boolean? = null, - signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (ProfileGalleryEntryEvent) -> Unit, - ) { - var etag = eventid?.let { arrayOf("e", it) } - relayhint?.let { etag = etag?.plus(it) } - - val tags = - listOfNotNull( - arrayOf(URL, url), - eventid?.let { etag }, - magnetUri?.let { arrayOf(MAGNET_URI, it) }, - mimeType?.let { arrayOf(MIME_TYPE, it) }, - alt?.ifBlank { null }?.let { arrayOf(ALT, it) } ?: AltTagSerializer.toTagArray(ALT_DESCRIPTION), - hash?.let { arrayOf(HASH, it) }, - size?.let { arrayOf(FILE_SIZE, it) }, - dimensions?.let { arrayOf(DIMENSION, it.toString()) }, - blurhash?.let { arrayOf(BLUR_HASH, it) }, - originalHash?.let { arrayOf(ORIGINAL_HASH, it) }, - magnetURI?.let { arrayOf(MAGNET_URI, it) }, - torrentInfoHash?.let { arrayOf(TORRENT_INFOHASH, it) }, - sensitiveContent?.let { - if (it) { - ContentWarningSerializer.toTagArray() - } else { - null - } - }, - ) - - val content = alt ?: "" - signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady) + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, "", createdAt) { + url(url) + alt(ALT_DESCRIPTION) + initializer() } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/profileGallery/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/profileGallery/TagArrayBuilderExt.kt new file mode 100644 index 000000000..8023ba0c5 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/profileGallery/TagArrayBuilderExt.kt @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.experimental.profileGallery + +import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.BlurhashTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.FallbackTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.HashTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.ImageTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.MagnetTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.MimeTypeTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.OriginalHashTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.ServiceTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.SizeTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.SummaryTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.ThumbTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.TorrentInfoHash +import com.vitorpamplona.quartz.nip94FileMetadata.tags.UrlTag + +fun TagArrayBuilder.url(url: String) = add(UrlTag.assemble(url)) + +fun TagArrayBuilder.mimeType(mimeType: String) = add(MimeTypeTag.assemble(mimeType)) + +fun TagArrayBuilder.hash(hash: HexKey) = add(HashTag.assemble(hash)) + +fun TagArrayBuilder.fileSize(size: Int) = add(SizeTag.assemble(size)) + +fun TagArrayBuilder.dimension(dim: DimensionTag) = add(DimensionTag.assemble(dim)) + +fun TagArrayBuilder.blurhash(blurhash: String) = add(BlurhashTag.assemble(blurhash)) + +fun TagArrayBuilder.originalHash(hash: HexKey) = add(OriginalHashTag.assemble(hash)) + +fun TagArrayBuilder.torrentInfohash(hash: String) = add(TorrentInfoHash.assemble(hash)) + +fun TagArrayBuilder.magnet(magnetUri: String) = add(MagnetTag.assemble(magnetUri)) + +fun TagArrayBuilder.image(imageUrl: String) = add(ImageTag.assemble(imageUrl)) + +fun TagArrayBuilder.thumb(trumbUrl: String) = add(ThumbTag.assemble(trumbUrl)) + +fun TagArrayBuilder.summary(summary: String) = add(SummaryTag.assemble(summary)) + +fun TagArrayBuilder.fallback(fallbackUrl: String) = add(FallbackTag.assemble(fallbackUrl)) + +fun TagArrayBuilder.service(service: String) = add(ServiceTag.assemble(service)) + +fun TagArrayBuilder.fromEvent( + event: HexKey, + relayHint: String?, +) = add(ETag.assemble(event, relayHint, null)) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/relationshipStatus/RelationshipStatusEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/relationshipStatus/RelationshipStatusEvent.kt index 90a24d87c..f4e70bf4f 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/relationshipStatus/RelationshipStatusEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/relationshipStatus/RelationshipStatusEvent.kt @@ -21,9 +21,16 @@ package com.vitorpamplona.quartz.experimental.relationshipStatus import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.experimental.relationshipStatus.tags.PetnameTag +import com.vitorpamplona.quartz.experimental.relationshipStatus.tags.RankTag +import com.vitorpamplona.quartz.experimental.relationshipStatus.tags.SummaryTag import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.core.tagArray import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer +import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag +import com.vitorpamplona.quartz.nip31Alts.alt +import com.vitorpamplona.quartz.nip51Lists.Nip51PrivateTags import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent import com.vitorpamplona.quartz.utils.TimeUtils @@ -36,48 +43,42 @@ class RelationshipStatusEvent( content: String, sig: HexKey, ) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig) { + fun rank() = tags.firstNotNullOfOrNull(RankTag::parse) + + fun petname() = tags.firstNotNullOfOrNull(PetnameTag::parse) + + fun summary() = tags.firstNotNullOfOrNull(SummaryTag::parse) + companion object { const val KIND = 30382 const val ALT = "Relationship Status" - const val PETNAME = "petname" - const val SUMMARY = "summary" - - private fun create( - content: String, - tags: Array>, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (RelationshipStatusEvent) -> Unit, - ) { - val newTags = - if (tags.any { it.size > 1 && it[0] == "alt" }) { - tags - } else { - tags + AltTagSerializer.toTagArray(ALT) - } - - signer.sign(createdAt, KIND, newTags, content, onReady) - } - fun create( targetUser: HexKey, petname: String? = null, summary: String? = null, signer: NostrSigner, createdAt: Long = TimeUtils.now(), + publicInitializer: TagArrayBuilder.() -> Unit = {}, + privateInitializer: TagArrayBuilder.() -> Unit = {}, onReady: (RelationshipStatusEvent) -> Unit, ) { - val tags = mutableListOf>() - tags.add(arrayOf("d", targetUser)) - tags.add(AltTagSerializer.toTagArray(ALT)) + val publicTags = + tagArray { + alt(ALT) + dTag(targetUser) + publicInitializer() + } - val privateTags = mutableListOf>() - petname?.let { privateTags.add(arrayOf(PETNAME, it)) } - summary?.let { privateTags.add(arrayOf(SUMMARY, it)) } + val privateTags = + tagArray { + petname?.let { petname(it) } + summary?.let { summary(it) } + privateInitializer() + } - encryptTags(privateTags.toTypedArray(), signer) { content -> - signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady) + Nip51PrivateTags.encryptNip44(privateTags, signer) { content -> + signer.sign(createdAt, KIND, publicTags, content, onReady) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/relationshipStatus/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/relationshipStatus/TagArrayBuilderExt.kt new file mode 100644 index 000000000..3d371ef77 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/relationshipStatus/TagArrayBuilderExt.kt @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.experimental.relationshipStatus + +import com.vitorpamplona.quartz.experimental.relationshipStatus.tags.PetnameTag +import com.vitorpamplona.quartz.experimental.relationshipStatus.tags.RankTag +import com.vitorpamplona.quartz.experimental.relationshipStatus.tags.SummaryTag +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder + +fun TagArrayBuilder.rank(rank: Int) = add(RankTag.assemble(rank)) + +fun TagArrayBuilder.petname(name: String) = add(PetnameTag.assemble(name)) + +fun TagArrayBuilder.summary(summary: String) = add(SummaryTag.assemble(summary)) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/relationshipStatus/tags/PetnameTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/relationshipStatus/tags/PetnameTag.kt new file mode 100644 index 000000000..df1ccfbfc --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/relationshipStatus/tags/PetnameTag.kt @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.experimental.relationshipStatus.tags + +class PetnameTag { + companion object { + const val TAG_NAME = "petname" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): String? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1] + } + + @JvmStatic + fun assemble(petname: String) = arrayOf(TAG_NAME, petname) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/relationshipStatus/tags/RankTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/relationshipStatus/tags/RankTag.kt new file mode 100644 index 000000000..3525445d0 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/relationshipStatus/tags/RankTag.kt @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.experimental.relationshipStatus.tags + +class RankTag { + companion object { + const val TAG_NAME = "rank" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): Int? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1].toIntOrNull() + } + + @JvmStatic + fun assemble(rank: Int) = arrayOf(TAG_NAME, rank.toString()) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/relationshipStatus/tags/SummaryTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/relationshipStatus/tags/SummaryTag.kt new file mode 100644 index 000000000..8a7696c29 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/relationshipStatus/tags/SummaryTag.kt @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.experimental.relationshipStatus.tags + +class SummaryTag { + companion object { + const val TAG_NAME = "summary" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): String? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1] + } + + @JvmStatic + fun assemble(ip: String) = arrayOf(TAG_NAME, ip) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/zapPolls/PollNoteEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/zapPolls/PollNoteEvent.kt index 63fe66daf..f9ad4057c 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/zapPolls/PollNoteEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/zapPolls/PollNoteEvent.kt @@ -21,132 +21,54 @@ package com.vitorpamplona.quartz.experimental.zapPolls import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.experimental.zapPolls.tags.ClosedAtTag +import com.vitorpamplona.quartz.experimental.zapPolls.tags.ConsensusThresholdTag +import com.vitorpamplona.quartz.experimental.zapPolls.tags.MaximumTag +import com.vitorpamplona.quartz.experimental.zapPolls.tags.MinimumTag +import com.vitorpamplona.quartz.experimental.zapPolls.tags.PollOptionTag import com.vitorpamplona.quartz.nip01Core.HexKey -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag -import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohashMipMap -import com.vitorpamplona.quartz.nip10Notes.BaseTextNoteEvent -import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrl -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer -import com.vitorpamplona.quartz.nip36SensitiveContent.ContentWarningSerializer -import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetup -import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetupSerializer -import com.vitorpamplona.quartz.nip57Zaps.zapraiser.ZapRaiserSerializer -import com.vitorpamplona.quartz.nip92IMeta.IMetaTag -import com.vitorpamplona.quartz.nip92IMeta.Nip92MediaAttachments +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate +import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent +import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.utils.TimeUtils -const val POLL_OPTION = "poll_option" -const val VALUE_MAXIMUM = "value_maximum" -const val VALUE_MINIMUM = "value_minimum" -const val CONSENSUS_THRESHOLD = "consensus_threshold" -const val CLOSED_AT = "closed_at" - @Immutable class PollNoteEvent( id: HexKey, pubKey: HexKey, createdAt: Long, tags: Array>, - // ots: String?, TODO implement OTS: https://github.com/opentimestamps/java-opentimestamps content: String, sig: HexKey, -) : BaseTextNoteEvent(id, pubKey, createdAt, KIND, tags, content, sig) { - fun pollOptions() = tags.filter { it.size > 2 && it[0] == POLL_OPTION }.associate { it[1].toInt() to it[2] } +) : BaseThreadedEvent(id, pubKey, createdAt, KIND, tags, content, sig) { + fun pollOptionsArray() = tags.mapNotNull(PollOptionTag::parse) - fun minimumAmount() = tags.firstOrNull { it.size > 1 && it[0] == VALUE_MINIMUM }?.getOrNull(1)?.toLongOrNull() + fun pollOptions() = pollOptionsArray().associate { it.index to it.descriptor } - fun maximumAmount() = tags.firstOrNull { it.size > 1 && it[0] == VALUE_MAXIMUM }?.getOrNull(1)?.toLongOrNull() + fun minAmount() = tags.firstNotNullOfOrNull(MinimumTag::parse) - fun getTagLong(property: String): Long? { - val number = tags.firstOrNull { it.size > 1 && it[0] == property }?.get(1) + fun maxAmount() = tags.firstNotNullOfOrNull(MaximumTag::parse) - return if (number.isNullOrBlank() || number == "null") { - null - } else { - number.toLong() - } - } + fun closedAt() = tags.firstNotNullOfOrNull(ClosedAtTag::parse) + + fun consensusThreshold() = tags.firstNotNullOfOrNull(ConsensusThresholdTag::parse) companion object { const val KIND = 6969 - const val ALT = "Poll event" + const val ALT_DESCRIPTION = "Poll event" - fun create( - msg: String, - replyTos: List?, - mentions: List?, - addresses: List?, - signer: NostrSigner, + fun build( + post: String, + options: List, createdAt: Long = TimeUtils.now(), - pollOptions: Map, - valueMaximum: Int?, - valueMinimum: Int?, - consensusThreshold: Int?, - closedAt: Int?, - zapReceiver: List? = null, - markAsSensitive: Boolean, - zapRaiserAmount: Long?, - geohash: String? = null, - imetas: List? = null, - emojis: List? = null, - isDraft: Boolean, - onReady: (PollNoteEvent) -> Unit, - ) { - val tags = mutableListOf>() - replyTos?.forEach { tags.add(arrayOf("e", it)) } - mentions?.forEach { tags.add(arrayOf("p", it)) } - addresses?.forEach { tags.add(arrayOf("a", it.toTag())) } - pollOptions.forEach { poll_op -> - tags.add(arrayOf(POLL_OPTION, poll_op.key.toString(), poll_op.value)) - } - valueMaximum?.let { tags.add(arrayOf(VALUE_MAXIMUM, valueMaximum.toString())) } - valueMinimum?.let { tags.add(arrayOf(VALUE_MINIMUM, valueMinimum.toString())) } - consensusThreshold?.let { - tags.add(arrayOf(CONSENSUS_THRESHOLD, consensusThreshold.toString())) - } - closedAt?.let { tags.add(arrayOf(CLOSED_AT, closedAt.toString())) } - zapReceiver?.forEach { tags.add(ZapSplitSetupSerializer.toTagArray(it)) } - zapRaiserAmount?.let { tags.add(ZapRaiserSerializer.toTagArray(it)) } - - if (markAsSensitive) { - tags.add(ContentWarningSerializer.toTagArray()) - } - geohash?.let { tags.addAll(geohashMipMap(it)) } - imetas?.forEach { - tags.add(Nip92MediaAttachments.createTag(it)) - } - emojis?.forEach { tags.add(it.toTagArray()) } - tags.add(AltTagSerializer.toTagArray(ALT)) - - if (isDraft) { - signer.assembleRumor(createdAt, KIND, tags.toTypedArray(), msg, onReady) - } else { - signer.sign(createdAt, KIND, tags.toTypedArray(), msg, onReady) - } + initializer: TagArrayBuilder.() -> Unit = {}, + ): EventTemplate { + val tags = TagArrayBuilder() + tags.pollOptions(options) + tags.alt(ALT_DESCRIPTION) + tags.apply(initializer) + return EventTemplate(createdAt, KIND, tags.build(), post) } } } - -/* -{ - "id": <32-bytes lowercase hex-encoded sha256 of the serialized event data> - "pubkey": <32-bytes lowercase hex-encoded public key of the event creator>, - "created_at": , - "kind": 6969, - "tags": [ - ["e", <32-bytes hex of the id of the poll event>, ], - ["p", <32-bytes hex of the key>, ], - ["poll_option", "0", "poll option 0 description string"], - ["poll_option", "1", "poll option 1 description string"], - ["poll_option", "n", "poll option description string"], - ["value_maximum", "maximum satoshi value for inclusion in tally"], - ["value_minimum", "minimum satoshi value for inclusion in tally"], - ["consensus_threshold", "required percentage to attain consensus <0..100>"], - ["closed_at", "unix timestamp in seconds"], - ], - "ots": - "content": , - "sig": <64-bytes hex of the signature of the sha256 hash of the serialized event data, which is the same as the "id" field> -} - */ diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/zapPolls/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/zapPolls/TagArrayBuilderExt.kt new file mode 100644 index 000000000..37b784d2a --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/zapPolls/TagArrayBuilderExt.kt @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.experimental.zapPolls + +import com.vitorpamplona.quartz.experimental.zapPolls.tags.ClosedAtTag +import com.vitorpamplona.quartz.experimental.zapPolls.tags.ConsensusThresholdTag +import com.vitorpamplona.quartz.experimental.zapPolls.tags.MaximumTag +import com.vitorpamplona.quartz.experimental.zapPolls.tags.MinimumTag +import com.vitorpamplona.quartz.experimental.zapPolls.tags.PollOptionTag +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder + +fun TagArrayBuilder.consensusThreshold(percentage: Double) = addUnique(ConsensusThresholdTag.assemble(percentage)) + +fun TagArrayBuilder.minAmount(value: Long) = addUnique(MinimumTag.assemble(value)) + +fun TagArrayBuilder.maxAmount(value: Long) = addUnique(MaximumTag.assemble(value)) + +fun TagArrayBuilder.closedAt(timestamp: Long) = addUnique(ClosedAtTag.assemble(timestamp)) + +fun TagArrayBuilder.pollOption( + index: Int, + description: String, +) = add(PollOptionTag.assemble(index, description)) + +fun TagArrayBuilder.pollOptions(options: Map) = addAll(options.map { PollOptionTag.assemble(it.key, it.value) }) + +fun TagArrayBuilder.pollOptions(options: List) = addAll(options.map { it.toTagArray() }) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/zapPolls/tags/ClosedAtTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/zapPolls/tags/ClosedAtTag.kt new file mode 100644 index 000000000..5b07019a8 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/zapPolls/tags/ClosedAtTag.kt @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.experimental.zapPolls.tags + +class ClosedAtTag { + companion object { + const val TAG_NAME = "closed_at" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): Long? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1].toLongOrNull() + } + + @JvmStatic + fun assemble(timestamp: Long) = arrayOf(TAG_NAME, timestamp.toString()) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/zapPolls/tags/ConsensusThresholdTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/zapPolls/tags/ConsensusThresholdTag.kt new file mode 100644 index 000000000..87e3967ec --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/zapPolls/tags/ConsensusThresholdTag.kt @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.experimental.zapPolls.tags + +import kotlin.math.round + +class ConsensusThresholdTag { + companion object { + const val TAG_NAME = "consensus_threshold" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): Double? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1].toLongOrNull()?.toDouble()?.div(100) + } + + @JvmStatic + fun assemble(percentage: Double) = arrayOf(TAG_NAME, (round(percentage * 100)).toString()) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/zapPolls/tags/MaximumTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/zapPolls/tags/MaximumTag.kt new file mode 100644 index 000000000..bf6381dee --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/zapPolls/tags/MaximumTag.kt @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.experimental.zapPolls.tags + +class MaximumTag { + companion object { + const val TAG_NAME = "value_maximum" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): Long? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1].toLongOrNull() + } + + @JvmStatic + fun assemble(timestamp: Long) = arrayOf(TAG_NAME, timestamp.toString()) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/zapPolls/tags/MinimumTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/zapPolls/tags/MinimumTag.kt new file mode 100644 index 000000000..7f1b8cfe8 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/zapPolls/tags/MinimumTag.kt @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.experimental.zapPolls.tags + +class MinimumTag { + companion object { + const val TAG_NAME = "value_minimum" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): Long? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1].toLongOrNull() + } + + @JvmStatic + fun assemble(timestamp: Long) = arrayOf(TAG_NAME, timestamp.toString()) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/zapPolls/tags/PollOptionTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/zapPolls/tags/PollOptionTag.kt new file mode 100644 index 000000000..f5a74b06d --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/zapPolls/tags/PollOptionTag.kt @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.experimental.zapPolls.tags + +class PollOptionTag( + val index: Int, + val descriptor: String, +) { + fun toTagArray() = assemble(index, descriptor) + + companion object { + const val TAG_NAME = "poll_option" + const val TAG_SIZE = 3 + + @JvmStatic + fun parse(tag: Array): PollOptionTag? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + + val index = tag[1].toIntOrNull() ?: return null + + return PollOptionTag(index, tag[2]) + } + + @JvmStatic + fun assemble( + index: Int, + descriptor: String, + ) = arrayOf(TAG_NAME, index.toString(), descriptor) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/EventExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/EventExt.kt index e77cacf12..e2e85884f 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/EventExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/EventExt.kt @@ -27,19 +27,19 @@ import com.vitorpamplona.quartz.utils.Hex fun Event.generateId(): String = EventHasher.hashId(pubKey, createdAt, kind, tags, content) -fun Event.hasCorrectIDHash(): Boolean { +fun Event.verifyId(): Boolean { if (id.isEmpty()) return false return id == generateId() } -fun Event.hasVerifiedSignature(): Boolean { +fun Event.verifySignature(): Boolean { if (id.isEmpty() || sig.isEmpty()) return false return CryptoUtils.verifySignature(Hex.decode(sig), Hex.decode(id), Hex.decode(pubKey)) } /** Checks if the ID is correct and then if the pubKey's secret key signed the event. */ fun Event.checkSignature() { - if (!hasCorrectIDHash()) { + if (!verifyId()) { throw Exception( """ |Unexpected ID. @@ -49,14 +49,14 @@ fun Event.checkSignature() { """.trimIndent(), ) } - if (!hasVerifiedSignature()) { + if (!verifySignature()) { throw Exception("""Bad signature!""") } } -fun Event.hasValidSignature(): Boolean = +fun Event.verify(): Boolean = try { - hasCorrectIDHash() && hasVerifiedSignature() + verifyId() && verifySignature() } catch (e: Exception) { Log.w("Event", "Event $id does not have a valid signature: ${toJson()}", e) false diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/EventHintBundle.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/EventHintBundle.kt index af8cda51e..0f875e2df 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/EventHintBundle.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/EventHintBundle.kt @@ -22,6 +22,11 @@ package com.vitorpamplona.quartz.nip01Core import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.pointerSizeInBytes @@ -31,9 +36,11 @@ data class EventHintBundle( val event: T, ) { var relay: String? = null + var authorHomeRelay: String? = null - constructor(event: T, relayHint: String? = null) : this(event) { + constructor(event: T, relayHint: String? = null, authorHomeRelay: String? = null) : this(event) { this.relay = relayHint + this.authorHomeRelay = authorHomeRelay } fun countMemory(): Long = @@ -43,9 +50,15 @@ data class EventHintBundle( fun toNEvent(): String = NEvent.create(event.id, event.pubKey, event.kind, relay) - fun toTagArray(tag: String) = listOfNotNull(tag, event.id, relay, event.pubKey).toTypedArray() + fun toETag() = ETag(event.id, relay, event.pubKey) - fun toETagArray() = toTagArray("e") + fun toATag() = ATag(event.kind, event.pubKey, event.dTag(), relay) - fun toQTagArray() = toTagArray("q") + fun toPTag() = PTag(event.pubKey, authorHomeRelay) + + fun toMarkedETag(marker: MarkedETag.MARKER) = MarkedETag(event.id, relay, marker, event.pubKey) + + fun toETagArray() = ETag.assemble(event.id, relay, event.pubKey) + + fun toQTagArray() = ETag(event.id, relay, event.pubKey).toQTagArray() } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/AddressableEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/AddressableEvent.kt index c0d1ab8b2..d57e5280e 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/AddressableEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/AddressableEvent.kt @@ -22,12 +22,15 @@ package com.vitorpamplona.quartz.nip01Core.core import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address @Immutable -interface AddressableEvent { +interface AddressableEvent : IEvent { fun dTag(): String - fun address(relayHint: String? = null): ATag + fun aTag(relayHint: String? = null): ATag + + fun address(): Address fun addressTag(): String } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/BaseAddressableEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/BaseAddressableEvent.kt index dd805b2c3..04d0a2d45 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/BaseAddressableEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/BaseAddressableEvent.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.quartz.nip01Core.core import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag @Immutable @@ -38,7 +39,9 @@ open class BaseAddressableEvent( AddressableEvent { override fun dTag() = tags.dTag() - override fun address(relayHint: String?) = ATag(kind, pubKey, dTag(), relayHint) + override fun aTag(relayHint: String?) = ATag(kind, pubKey, dTag(), relayHint) + + override fun address() = Address(kind, pubKey, dTag()) /** * Creates the tag in a memory efficient way (without creating the ATag class diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/BaseReplaceableEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/BaseReplaceableEvent.kt index 0968f210e..9535c08bb 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/BaseReplaceableEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/BaseReplaceableEvent.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.quartz.nip01Core.core import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address @Immutable open class BaseReplaceableEvent( @@ -36,7 +37,9 @@ open class BaseReplaceableEvent( ) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) { override fun dTag() = FIXED_D_TAG - override fun address(relayHint: String?) = ATag(kind, pubKey, FIXED_D_TAG, relayHint) + override fun aTag(relayHint: String?) = ATag(kind, pubKey, FIXED_D_TAG, relayHint) + + override fun address() = Address(kind, pubKey, dTag()) /** * Creates the tag in a memory efficient way (without creating the ATag class diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/Event.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/Event.kt index 60920971d..cb2c863e6 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/Event.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/Event.kt @@ -39,7 +39,7 @@ open class Event( val tags: TagArray, val content: String, val sig: HexKey, -) { +) : IEvent { open fun isContentEncoded() = false open fun countMemory(): Long = diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip99Classifieds/Price.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/IEvent.kt similarity index 88% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip99Classifieds/Price.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/IEvent.kt index 1c899002a..66d87fc14 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip99Classifieds/Price.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/IEvent.kt @@ -18,10 +18,6 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip99Classifieds +package com.vitorpamplona.quartz.nip01Core.core -data class Price( - val amount: String, - val currency: String?, - val frequency: String?, -) +interface IEvent diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/Tag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/Tag.kt new file mode 100644 index 000000000..71e22c657 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/Tag.kt @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.core + +typealias Tag = Array + +fun Tag.name() = this[0] + +fun Tag.value() = this[1] + +fun Tag.hasValue() = this[1].isNotEmpty() + +fun Tag.nameOrNull() = if (size > 0) name() else null + +fun Tag.valueOrNull() = if (size > 1) value() else null + +fun Tag.isNameUnsafe(name: String): Boolean = name() == name + +fun Tag.isValueUnsafe(value: String): Boolean = value() == value + +fun Tag.isValueInUnsafe(values: Set): Boolean = value() in values + +fun Tag.match(name: String): Boolean = if (size > 0) isNameUnsafe(name) else false + +fun Tag.isValue(value: String): Boolean = if (size > 1) isValueUnsafe(value) else false + +fun Tag.match( + name: String, + value: String, + minSize: Int, +): Boolean = if (size >= minSize) isNameUnsafe(name) && isValueUnsafe(value) else false + +fun Tag.match( + name: String, + values: Set, + minSize: Int, +): Boolean = if (size >= minSize) isNameUnsafe(name) && isValueInUnsafe(values) else false + +fun Tag.match( + name: String, + minSize: Int, +): Boolean = if (size >= minSize) isNameUnsafe(name) else false + +fun Tag.isNotName( + name: String, + minSize: Int, +): Boolean = !match(name, minSize) + +fun Tag.matchAndHasValue( + name: String, + minSize: Int, +): Boolean = if (size >= minSize) isNameUnsafe(name) && hasValue() else false + +fun Tag.valueIfMatches( + name: String, + minSize: Int, +): String? = if (match(name, minSize)) value() else null + +fun Tag.valueToIntIfMatches( + name: String, + minSize: Int, +): Int? = if (match(name, minSize)) value().toIntOrNull() else null diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/TagArray.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/TagArray.kt index aa665d136..77f49af17 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/TagArray.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/TagArray.kt @@ -24,6 +24,8 @@ import com.vitorpamplona.quartz.nip01Core.HexKey typealias TagArray = Array> +fun TagArray.builder(initializer: TagArrayBuilder.() -> Unit = {}) = TagArrayBuilder().addAll(this).apply(initializer).build() + /** * Performs the given [action] on each tag that matches the given [tagName]. */ @@ -109,7 +111,7 @@ fun TagArray.mapValues(tagName: String) = * Returns the first non-null value produced by [transform] function being applied to all tags * that match the [tagName] */ -fun TagArray.firstMapTagged( +fun TagArray.firstMappedTag( tagName: String, transform: (tagValue: Array) -> R, ) = this.firstNotNullOfOrNull { @@ -161,7 +163,8 @@ fun TagArray.firstTagValueFor(vararg tagNames: String) = this.firstOrNull { it.s fun TagArray.isTagged( tagName: String, tagValue: String, -) = this.any { it.size > 1 && it[0] == tagName && it[1] == tagValue } + ignoreCase: Boolean = false, +) = this.any { it.size > 1 && it[0] == tagName && it[1].equals(tagValue, ignoreCase) } /** * Returns `true` if at least one tag matches the given [tagName] and is in [tagValues] @@ -171,6 +174,51 @@ fun TagArray.isAnyTagged( tagValues: Set, ) = this.any { it.size > 1 && it[0] == tagName && it[1] in tagValues } +fun TagArray.any( + predicate: (Array, U) -> Boolean, + extras: U, +): Boolean { + for (element in this) if (predicate(element, extras)) return true + return false +} + +public inline fun Array.firstNotNullOfOrNull( + transform: (T, U) -> R?, + extras: U, +): R? { + for (element in this) { + val result = transform(element, extras) + if (result != null) { + return result + } + } + return null +} + +/** + * Returns `true` if at least one tag matches the given [tagName] and is in [tagValues] + */ +fun TagArray.firstAnyLowercaseTaggedValue( + tagName: String, + tagValues: Set, +) = this.firstOrNull { it.size > 1 && it[0] == tagName && it[1].lowercase() in tagValues }?.getOrNull(1) + +/** + * Returns `true` if at least one tag matches the given [tagName] and is in [tagValues] + */ +fun TagArray.isAnyLowercaseTagged( + tagName: String, + tagValues: Set, +) = this.any { it.size > 1 && it[0] == tagName && it[1].lowercase() in tagValues } + +/** + * Returns `true` if at least one tag matches the given [tagName] and is in [tagValues] + */ +fun TagArray.firstAnyTaggedValue( + tagName: String, + tagValues: Set, +) = this.firstOrNull { it.size > 1 && it[0] == tagName && it[1] in tagValues }?.getOrNull(1) + /** * Returns `true` if at least one tag has value that contains [text] */ @@ -178,3 +226,15 @@ fun TagArray.tagValueContains( text: String, ignoreCase: Boolean = false, ) = this.any { it.size > 1 && it[1].contains(text, ignoreCase) } + +fun TagArray.containsAllTagNamesWithValues(names: Set): Boolean { + val remaining = names.toMutableSet() + + this.forEach { + if (it.size > 1) { + remaining.remove(it[0]) + } + } + + return remaining.isEmpty() +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/TagArrayBuilder.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/TagArrayBuilder.kt index 1c0188f81..3d03f667b 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/TagArrayBuilder.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/TagArrayBuilder.kt @@ -20,23 +20,75 @@ */ package com.vitorpamplona.quartz.nip01Core.core -class TagArrayBuilder { - val tagList = mutableListOf>() +class TagArrayBuilder { + /** + * keeps a tag list by tag names to treat tags that must be unique + */ + private val tagList = mutableMapOf>() - fun add(tag: Array): TagArrayBuilder { - tagList.add(tag) + fun remove(tagName: String): TagArrayBuilder { + tagList.remove(tagName) return this } - fun addAll(tag: List>): TagArrayBuilder { - tagList.addAll(tag) + fun remove( + tagName: String, + tagValue: String, + ): TagArrayBuilder { + tagList[tagName]?.removeIf { it.value() == tagValue } + if (tagList[tagName]?.isEmpty() == true) { + tagList.remove(tagName) + } return this } - fun addAll(tag: Array>): TagArrayBuilder { - tagList.addAll(tag) + fun removeIf( + predicate: (Tag, Tag) -> Boolean, + toCompare: Tag, + ): TagArrayBuilder { + tagList[toCompare.name()]?.removeIf { predicate(it, toCompare) } + if (tagList[toCompare.name()]?.isEmpty() == true) { + tagList.remove(toCompare.name()) + } return this } - fun build() = tagList.toTypedArray() + fun removeIf( + tagName: String, + tagValue: String, + ): TagArrayBuilder { + tagList[tagName]?.removeIf { it.value() == tagValue } + if (tagList[tagName]?.isEmpty() == true) { + tagList.remove(tagName) + } + return this + } + + fun add(tag: Array): TagArrayBuilder { + if (tag.isEmpty() || tag[0].isEmpty()) return this + tagList.getOrPut(tag[0], ::mutableListOf).add(tag) + return this + } + + fun addUnique(tag: Array): TagArrayBuilder { + if (tag.isEmpty() || tag[0].isEmpty()) return this + tagList[tag[0]] = mutableListOf(tag) + return this + } + + fun addAll(tag: List>): TagArrayBuilder { + tag.forEach(::add) + return this + } + + fun addAll(tag: Array>): TagArrayBuilder { + tag.forEach(::add) + return this + } + + fun toTypedArray() = tagList.flatMap { it.value }.toTypedArray() + + fun build() = toTypedArray() } + +inline fun tagArray(initializer: TagArrayBuilder.() -> Unit = {}): TagArray = TagArrayBuilder().apply(initializer).build() diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/KeyPair.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/crypto/KeyPair.kt similarity index 95% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/KeyPair.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/crypto/KeyPair.kt index f90e57607..a5ac5c4ac 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/KeyPair.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/crypto/KeyPair.kt @@ -18,9 +18,10 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip01Core +package com.vitorpamplona.quartz.nip01Core.crypto import com.vitorpamplona.quartz.CryptoUtils +import com.vitorpamplona.quartz.nip01Core.toHexKey class KeyPair( privKey: ByteArray? = null, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/Nip01.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/crypto/Nip01.kt similarity index 98% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/Nip01.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/crypto/Nip01.kt index 3e2f0cb01..6466d3c75 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/Nip01.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/crypto/Nip01.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip01Core +package com.vitorpamplona.quartz.nip01Core.crypto import com.vitorpamplona.quartz.CryptoUtils import com.vitorpamplona.quartz.utils.nextBytes diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/EventMapper.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/EventMapper.kt index fe6090dd7..f3a41e3fd 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/EventMapper.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/EventMapper.kt @@ -35,9 +35,9 @@ import com.vitorpamplona.quartz.nip47WalletConnect.Request import com.vitorpamplona.quartz.nip47WalletConnect.RequestDeserializer import com.vitorpamplona.quartz.nip47WalletConnect.Response import com.vitorpamplona.quartz.nip47WalletConnect.ResponseDeserializer -import com.vitorpamplona.quartz.nip59Giftwrap.Rumor -import com.vitorpamplona.quartz.nip59Giftwrap.RumorDeserializer -import com.vitorpamplona.quartz.nip59Giftwrap.RumorSerializer +import com.vitorpamplona.quartz.nip59Giftwrap.rumors.Rumor +import com.vitorpamplona.quartz.nip59Giftwrap.rumors.RumorDeserializer +import com.vitorpamplona.quartz.nip59Giftwrap.rumors.RumorSerializer class EventMapper { companion object { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/MetadataEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/metadata/MetadataEvent.kt similarity index 93% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/MetadataEvent.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/metadata/MetadataEvent.kt index f62f9cc68..dec616521 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/MetadataEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/metadata/MetadataEvent.kt @@ -18,17 +18,18 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip01Core +package com.vitorpamplona.quartz.nip01Core.metadata import android.util.Log import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.node.ObjectNode +import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync import com.vitorpamplona.quartz.nip21UriScheme.toNostrUri -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer +import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.nip39ExtIdentities.updateClaims import com.vitorpamplona.quartz.utils.TimeUtils import java.io.ByteArrayInputStream @@ -69,7 +70,7 @@ class MetadataEvent( val tags = mutableListOf>() tags.add( - AltTagSerializer.toTagArray("User profile for ${name ?: currentJson.get("name").asText() ?: ""}"), + AltTag.assemble("User profile for ${name ?: currentJson.get("name").asText() ?: ""}"), ) return signer.sign(createdAt, KIND, tags.toTypedArray(), writer.buffer.toString()) @@ -119,7 +120,7 @@ class MetadataEvent( ObjectMapper().writeValue(writer, currentJson) val tags = mutableListOf>() - tags.add(AltTagSerializer.toTagArray("User profile for ${name ?: currentJson.get("name").asText() ?: ""}")) + tags.add(AltTag.assemble("User profile for ${name ?: currentJson.get("name").asText() ?: ""}")) latest?.updateClaims(twitter, github, mastodon)?.forEach { tags.add(it) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/UserMetadata.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/metadata/UserMetadata.kt similarity index 98% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/UserMetadata.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/metadata/UserMetadata.kt index 6fe44d511..10c8513df 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/UserMetadata.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/metadata/UserMetadata.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip01Core +package com.vitorpamplona.quartz.nip01Core.metadata import androidx.compose.runtime.Stable import com.fasterxml.jackson.annotation.JsonProperty diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/RelayStat.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/RelayStat.kt similarity index 98% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/RelayStat.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/RelayStat.kt index c63fa52db..fa7870141 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/RelayStat.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/RelayStat.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip01Core.relays +package com.vitorpamplona.quartz.nip01Core.relay import androidx.collection.LruCache import com.vitorpamplona.quartz.utils.TimeUtils diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/RelayState.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/RelayState.kt similarity index 96% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/RelayState.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/RelayState.kt index caf9fb925..49ea625d3 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/RelayState.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/RelayState.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip01Core.relays +package com.vitorpamplona.quartz.nip01Core.relay enum class RelayState { // Websocket connected diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/SimpleClientRelay.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/SimpleClientRelay.kt similarity index 92% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/SimpleClientRelay.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/SimpleClientRelay.kt index 12c2477dd..ecbe9a21f 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/SimpleClientRelay.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/SimpleClientRelay.kt @@ -18,27 +18,27 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip01Core.relays +package com.vitorpamplona.quartz.nip01Core.relay import android.util.Log import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.relays.commands.toClient.AuthMessage -import com.vitorpamplona.quartz.nip01Core.relays.commands.toClient.ClosedMessage -import com.vitorpamplona.quartz.nip01Core.relays.commands.toClient.EoseMessage -import com.vitorpamplona.quartz.nip01Core.relays.commands.toClient.EventMessage -import com.vitorpamplona.quartz.nip01Core.relays.commands.toClient.NoticeMessage -import com.vitorpamplona.quartz.nip01Core.relays.commands.toClient.NotifyMessage -import com.vitorpamplona.quartz.nip01Core.relays.commands.toClient.OkMessage -import com.vitorpamplona.quartz.nip01Core.relays.commands.toClient.ToClientParser -import com.vitorpamplona.quartz.nip01Core.relays.commands.toRelay.AuthCmd -import com.vitorpamplona.quartz.nip01Core.relays.commands.toRelay.CloseCmd -import com.vitorpamplona.quartz.nip01Core.relays.commands.toRelay.CountCmd -import com.vitorpamplona.quartz.nip01Core.relays.commands.toRelay.EventCmd -import com.vitorpamplona.quartz.nip01Core.relays.filters.Filter -import com.vitorpamplona.quartz.nip01Core.relays.sockets.WebSocket -import com.vitorpamplona.quartz.nip01Core.relays.sockets.WebSocketListener -import com.vitorpamplona.quartz.nip01Core.relays.sockets.WebsocketBuilder +import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.AuthMessage +import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.ClosedMessage +import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.EoseMessage +import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.EventMessage +import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.NoticeMessage +import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.NotifyMessage +import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.OkMessage +import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.ToClientParser +import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.AuthCmd +import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.CloseCmd +import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.CountCmd +import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.EventCmd +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebSocket +import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebSocketListener +import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebsocketBuilder import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.bytesUsedInMemory @@ -313,7 +313,7 @@ class SimpleClientRelay( if (isReady) { if (filters.isNotEmpty()) { writeToSocket( - com.vitorpamplona.quartz.nip01Core.relays.commands.toRelay.ReqCmd + com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.ReqCmd .toJson(requestId, filters), ) afterEOSEPerSubscription[requestId] = false diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/Subscription.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/Subscription.kt similarity index 91% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/Subscription.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/Subscription.kt index 5a83bc4af..3f06060b2 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/Subscription.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/Subscription.kt @@ -18,9 +18,9 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip01Core.relays +package com.vitorpamplona.quartz.nip01Core.relay -import com.vitorpamplona.quartz.nip01Core.relays.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import java.util.UUID class Subscription( diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/SubscriptionCollection.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/SubscriptionCollection.kt similarity index 92% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/SubscriptionCollection.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/SubscriptionCollection.kt index b1d4252c0..3f2346bdd 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/SubscriptionCollection.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/SubscriptionCollection.kt @@ -18,10 +18,10 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip01Core.relays +package com.vitorpamplona.quartz.nip01Core.relay import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.relays.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter interface SubscriptionCollection { fun isActive(subscriptionId: String): Boolean diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/commands/toClient/AuthMessage.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toClient/AuthMessage.kt similarity index 95% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/commands/toClient/AuthMessage.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toClient/AuthMessage.kt index ff45dda0c..c1bff67b6 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/commands/toClient/AuthMessage.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toClient/AuthMessage.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip01Core.relays.commands.toClient +package com.vitorpamplona.quartz.nip01Core.relay.commands.toClient import com.fasterxml.jackson.databind.JsonNode diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/commands/toClient/ClosedMessage.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toClient/ClosedMessage.kt similarity index 95% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/commands/toClient/ClosedMessage.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toClient/ClosedMessage.kt index 12aae77f7..8cf16f807 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/commands/toClient/ClosedMessage.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toClient/ClosedMessage.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip01Core.relays.commands.toClient +package com.vitorpamplona.quartz.nip01Core.relay.commands.toClient import com.fasterxml.jackson.databind.JsonNode diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/commands/toClient/EoseMessage.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toClient/EoseMessage.kt similarity index 95% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/commands/toClient/EoseMessage.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toClient/EoseMessage.kt index fcc100b9b..455090480 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/commands/toClient/EoseMessage.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toClient/EoseMessage.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip01Core.relays.commands.toClient +package com.vitorpamplona.quartz.nip01Core.relay.commands.toClient import com.fasterxml.jackson.databind.JsonNode diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/commands/toClient/EventMessage.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toClient/EventMessage.kt similarity index 96% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/commands/toClient/EventMessage.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toClient/EventMessage.kt index 2f5f60f87..f4957abd3 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/commands/toClient/EventMessage.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toClient/EventMessage.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip01Core.relays.commands.toClient +package com.vitorpamplona.quartz.nip01Core.relay.commands.toClient import com.fasterxml.jackson.databind.JsonNode import com.vitorpamplona.quartz.nip01Core.core.Event diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/commands/toClient/Message.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toClient/Message.kt similarity index 94% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/commands/toClient/Message.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toClient/Message.kt index a025f5528..64dcfcc73 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/commands/toClient/Message.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toClient/Message.kt @@ -18,6 +18,6 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip01Core.relays.commands.toClient +package com.vitorpamplona.quartz.nip01Core.relay.commands.toClient interface Message diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/commands/toClient/NoticeMessage.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toClient/NoticeMessage.kt similarity index 95% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/commands/toClient/NoticeMessage.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toClient/NoticeMessage.kt index 7ad6a35b6..eff9c10a5 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/commands/toClient/NoticeMessage.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toClient/NoticeMessage.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip01Core.relays.commands.toClient +package com.vitorpamplona.quartz.nip01Core.relay.commands.toClient import com.fasterxml.jackson.databind.JsonNode diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/commands/toClient/NotifyMessage.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toClient/NotifyMessage.kt similarity index 95% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/commands/toClient/NotifyMessage.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toClient/NotifyMessage.kt index 06ece9763..30ace93e7 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/commands/toClient/NotifyMessage.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toClient/NotifyMessage.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip01Core.relays.commands.toClient +package com.vitorpamplona.quartz.nip01Core.relay.commands.toClient import com.fasterxml.jackson.databind.JsonNode diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/commands/toClient/OkMessage.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toClient/OkMessage.kt similarity index 96% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/commands/toClient/OkMessage.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toClient/OkMessage.kt index bbcdbea58..0d494bf1d 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/commands/toClient/OkMessage.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toClient/OkMessage.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip01Core.relays.commands.toClient +package com.vitorpamplona.quartz.nip01Core.relay.commands.toClient import com.fasterxml.jackson.databind.JsonNode import com.vitorpamplona.quartz.nip01Core.HexKey diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/commands/toClient/ToClientParser.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toClient/ToClientParser.kt similarity index 96% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/commands/toClient/ToClientParser.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toClient/ToClientParser.kt index 4abf3f552..ea654aeba 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/commands/toClient/ToClientParser.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toClient/ToClientParser.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip01Core.relays.commands.toClient +package com.vitorpamplona.quartz.nip01Core.relay.commands.toClient import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/commands/toRelay/AuthCmd.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toRelay/AuthCmd.kt similarity index 96% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/commands/toRelay/AuthCmd.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toRelay/AuthCmd.kt index a9451858c..fd35bb14d 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/commands/toRelay/AuthCmd.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toRelay/AuthCmd.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip01Core.relays.commands.toRelay +package com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay import com.fasterxml.jackson.databind.JsonNode import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/commands/toRelay/CloseCmd.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toRelay/CloseCmd.kt similarity index 95% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/commands/toRelay/CloseCmd.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toRelay/CloseCmd.kt index ba44bff65..c2f5d08ac 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/commands/toRelay/CloseCmd.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toRelay/CloseCmd.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip01Core.relays.commands.toRelay +package com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay import com.fasterxml.jackson.databind.JsonNode diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/commands/toRelay/Command.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toRelay/Command.kt similarity index 94% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/commands/toRelay/Command.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toRelay/Command.kt index b68b114bf..50c4c7d8f 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/commands/toRelay/Command.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toRelay/Command.kt @@ -18,6 +18,6 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip01Core.relays.commands.toRelay +package com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay interface Command diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/commands/toRelay/CountCmd.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toRelay/CountCmd.kt similarity index 91% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/commands/toRelay/CountCmd.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toRelay/CountCmd.kt index 36a811808..b0ea5d1e0 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/commands/toRelay/CountCmd.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toRelay/CountCmd.kt @@ -18,13 +18,13 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip01Core.relays.commands.toRelay +package com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.node.ObjectNode import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper -import com.vitorpamplona.quartz.nip01Core.relays.filters.Filter -import com.vitorpamplona.quartz.nip01Core.relays.filters.FilterDeserializer +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.filters.FilterDeserializer import com.vitorpamplona.quartz.utils.joinToStringLimited class CountCmd( diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/commands/toRelay/EventCmd.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toRelay/EventCmd.kt similarity index 96% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/commands/toRelay/EventCmd.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toRelay/EventCmd.kt index 54f33ae70..002e17028 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/commands/toRelay/EventCmd.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toRelay/EventCmd.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip01Core.relays.commands.toRelay +package com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay import com.fasterxml.jackson.databind.JsonNode import com.vitorpamplona.quartz.nip01Core.core.Event diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/commands/toRelay/ReqCmd.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toRelay/ReqCmd.kt similarity index 85% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/commands/toRelay/ReqCmd.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toRelay/ReqCmd.kt index 2e703d860..7d93b978f 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/commands/toRelay/ReqCmd.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toRelay/ReqCmd.kt @@ -18,19 +18,19 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip01Core.relays.commands.toRelay +package com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.node.ObjectNode import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper -import com.vitorpamplona.quartz.nip01Core.relays.filters.Filter -import com.vitorpamplona.quartz.nip01Core.relays.filters.FilterDeserializer +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.filters.FilterDeserializer import com.vitorpamplona.quartz.utils.joinToStringLimited class ReqCmd( val subscriptionId: String, val filters: List, -) : com.vitorpamplona.quartz.nip01Core.relays.commands.toRelay.Command { +) : com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.Command { companion object { const val LABEL = "REQ" @@ -50,7 +50,7 @@ class ReqCmd( } @JvmStatic - fun parse(msgArray: JsonNode): com.vitorpamplona.quartz.nip01Core.relays.commands.toRelay.ReqCmd { + fun parse(msgArray: JsonNode): com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.ReqCmd { val filters = mutableListOf() for (i in 2 until msgArray.size()) { @@ -60,7 +60,7 @@ class ReqCmd( } } - return com.vitorpamplona.quartz.nip01Core.relays.commands.toRelay.ReqCmd( + return com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.ReqCmd( msgArray.get(1).asText(), filters, ) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/commands/toRelay/ToRelayParser.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toRelay/ToRelayParser.kt similarity index 96% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/commands/toRelay/ToRelayParser.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toRelay/ToRelayParser.kt index ba45a5020..b90ad14e4 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/commands/toRelay/ToRelayParser.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toRelay/ToRelayParser.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip01Core.relays.commands.toRelay +package com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/filters/Filter.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/filters/Filter.kt similarity index 97% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/filters/Filter.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/filters/Filter.kt index 7bd48bd03..ec7864c08 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/filters/Filter.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/filters/Filter.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip01Core.relays.filters +package com.vitorpamplona.quartz.nip01Core.relay.filters import com.vitorpamplona.quartz.nip01Core.core.Event diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/filters/FilterDeserializer.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/filters/FilterDeserializer.kt similarity index 97% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/filters/FilterDeserializer.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/filters/FilterDeserializer.kt index 08260a6d9..fd5b1a105 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/filters/FilterDeserializer.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/filters/FilterDeserializer.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip01Core.relays.filters +package com.vitorpamplona.quartz.nip01Core.relay.filters import com.fasterxml.jackson.databind.node.ObjectNode diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/filters/FilterMatcher.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/filters/FilterMatcher.kt similarity index 97% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/filters/FilterMatcher.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/filters/FilterMatcher.kt index 7a0bbb43c..c354ec4d4 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/filters/FilterMatcher.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/filters/FilterMatcher.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip01Core.relays.filters +package com.vitorpamplona.quartz.nip01Core.relay.filters import com.vitorpamplona.quartz.nip01Core.core.Event diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/filters/FilterSerializer.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/filters/FilterSerializer.kt similarity index 98% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/filters/FilterSerializer.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/filters/FilterSerializer.kt index 145c8798a..830a0eb86 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/filters/FilterSerializer.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/filters/FilterSerializer.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip01Core.relays.filters +package com.vitorpamplona.quartz.nip01Core.relay.filters import com.fasterxml.jackson.databind.node.JsonNodeFactory import com.fasterxml.jackson.databind.node.ObjectNode diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/sockets/WebSocket.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/sockets/WebSocket.kt similarity index 95% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/sockets/WebSocket.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/sockets/WebSocket.kt index 9fa6598bb..36387e350 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/sockets/WebSocket.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/sockets/WebSocket.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip01Core.relays.sockets +package com.vitorpamplona.quartz.nip01Core.relay.sockets interface WebSocket { fun connect() diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/sockets/WebSocketListener.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/sockets/WebSocketListener.kt similarity index 96% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/sockets/WebSocketListener.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/sockets/WebSocketListener.kt index 69dadfb1d..8d6499d2a 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/sockets/WebSocketListener.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/sockets/WebSocketListener.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip01Core.relays.sockets +package com.vitorpamplona.quartz.nip01Core.relay.sockets interface WebSocketListener { fun onOpen( diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/sockets/WebsocketBuilder.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/sockets/WebsocketBuilder.kt similarity index 95% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/sockets/WebsocketBuilder.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/sockets/WebsocketBuilder.kt index e8fd6960e..a5c11a167 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/sockets/WebsocketBuilder.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/sockets/WebsocketBuilder.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip01Core.relays.sockets +package com.vitorpamplona.quartz.nip01Core.relay.sockets interface WebsocketBuilder { fun build( diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/sockets/WebsocketBuilderFactory.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/sockets/WebsocketBuilderFactory.kt similarity index 95% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/sockets/WebsocketBuilderFactory.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/sockets/WebsocketBuilderFactory.kt index 3a4a50f14..1cbcdddd9 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relays/sockets/WebsocketBuilderFactory.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/sockets/WebsocketBuilderFactory.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip01Core.relays.sockets +package com.vitorpamplona.quartz.nip01Core.relay.sockets interface WebsocketBuilderFactory { fun build(forceProxy: Boolean): WebsocketBuilder diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/EventTemplate.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/EventTemplate.kt new file mode 100644 index 000000000..60d7527d2 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/EventTemplate.kt @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.signers + +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.TagArray +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.core.builder +import com.vitorpamplona.quartz.nip01Core.core.tagArray +import com.vitorpamplona.quartz.utils.TimeUtils + +class EventTemplate( + val createdAt: Long, + val kind: Int, + val tags: TagArray, + val content: String, +) + +inline fun eventTemplate( + kind: Int, + description: String, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, +) = EventTemplate(createdAt, kind, tagArray(initializer), description) + +fun eventUpdate( + base: Event, + createdAt: Long = TimeUtils.now(), + updater: TagArrayBuilder.() -> Unit = {}, +) = EventTemplate(createdAt, base.kind, base.tags.builder(updater), base.content) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/NostrSigner.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/NostrSigner.kt index d359c1f46..bfda070b0 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/NostrSigner.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/NostrSigner.kt @@ -24,13 +24,18 @@ import com.vitorpamplona.quartz.EventFactory import com.vitorpamplona.quartz.nip01Core.EventHasher import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip04Dm.Nip04 +import com.vitorpamplona.quartz.nip04Dm.Nip04Encryption import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent abstract class NostrSigner( val pubKey: HexKey, ) { + fun sign( + ev: EventTemplate, + onReady: (T) -> Unit, + ) = sign(ev.createdAt, ev.kind, ev.tags, ev.content, onReady) + abstract fun sign( createdAt: Long, kind: Int, @@ -73,13 +78,18 @@ abstract class NostrSigner( fromPublicKey: HexKey, onReady: (String) -> Unit, ) { - if (Nip04.isNIP04(encryptedContent)) { + if (Nip04Encryption.isNIP04(encryptedContent)) { nip04Decrypt(encryptedContent, fromPublicKey, onReady) } else { nip44Decrypt(encryptedContent, fromPublicKey, onReady) } } + fun assembleRumor( + ev: EventTemplate, + onReady: (T) -> Unit, + ) = assembleRumor(ev.createdAt, ev.kind, ev.tags, ev.content, onReady) + fun assembleRumor( createdAt: Long, kind: Int, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/NostrSignerInternal.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/NostrSignerInternal.kt index b56aff320..70da4e2f4 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/NostrSignerInternal.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/NostrSignerInternal.kt @@ -21,8 +21,8 @@ package com.vitorpamplona.quartz.nip01Core.signers import com.vitorpamplona.quartz.nip01Core.HexKey -import com.vitorpamplona.quartz.nip01Core.KeyPair import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.toHexKey import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/NostrSignerSync.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/NostrSignerSync.kt index 0d633cd4e..aabdafd1c 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/NostrSignerSync.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/NostrSignerSync.kt @@ -25,8 +25,8 @@ import com.vitorpamplona.quartz.CryptoUtils import com.vitorpamplona.quartz.EventFactory import com.vitorpamplona.quartz.nip01Core.EventHasher import com.vitorpamplona.quartz.nip01Core.HexKey -import com.vitorpamplona.quartz.nip01Core.KeyPair import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.hexToByteArray import com.vitorpamplona.quartz.nip01Core.toHexKey import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/ATag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/ATag.kt index 2752f1914..a6decad3f 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/ATag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/ATag.kt @@ -20,11 +20,11 @@ */ package com.vitorpamplona.quartz.nip01Core.tags.addressables -import android.util.Log import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.HexKey -import com.vitorpamplona.quartz.utils.Hex -import com.vitorpamplona.quartz.utils.arrayOfNotNull +import com.vitorpamplona.quartz.nip01Core.core.match +import com.vitorpamplona.quartz.nip01Core.core.name +import com.vitorpamplona.quartz.nip01Core.core.value import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.pointerSizeInBytes import com.vitorpamplona.quartz.utils.removeTrailingNullsAndEmptyOthers @@ -37,6 +37,8 @@ data class ATag( ) { var relay: String? = null + constructor(address: Address) : this(address.kind, address.pubKeyHex, address.dTag) + constructor( kind: Int, pubKeyHex: HexKey, @@ -61,48 +63,84 @@ data class ATag( companion object { const val TAG_NAME = "a" + const val TAG_SIZE = 2 + @JvmStatic + fun isSameAddress( + tag1: Array, + tag2: Array, + ) = tag1.match(tag2.name(), tag2.value(), TAG_SIZE) + + @JvmStatic + fun isTagged( + tag: Array, + addressId: String, + ) = ATagParser.isTagged(tag, TAG_NAME, addressId) + + @JvmStatic + fun isTagged( + tag: Array, + address: ATag, + ) = ATagParser.isTagged(tag, TAG_NAME, address.toTag()) + + @JvmStatic + fun isIn( + tag: Array, + addressIds: Set, + ) = ATagParser.isIn(tag, TAG_NAME, addressIds) + + @JvmStatic + fun isTaggedWithKind( + tag: Array, + kind: String, + ) = ATagParser.isTaggedWithKind(tag, TAG_NAME, kind) + + @JvmStatic fun assembleATagId( kind: Int, - pubKeyHex: HexKey, + pubKey: HexKey, dTag: String, - ) = "$kind:$pubKeyHex:$dTag" + ) = ATagParser.assembleATagId(kind, pubKey, dTag) + @JvmStatic + fun parseIfOfKind( + tag: Array, + kind: String, + ) = ATagParser.parseIfOfKind(tag, TAG_NAME, kind) + + @JvmStatic + fun parseIfIsIn( + tag: Array, + addresses: Set, + ) = ATagParser.parseIfIsIn(tag, TAG_NAME, addresses) + + @JvmStatic fun parse( aTagId: String, relay: String?, - ): ATag? = - try { - val parts = aTagId.split(":", limit = 3) - if (Hex.isHex(parts[1])) { - ATag(parts[0].toInt(), parts[1], parts[2], relay) - } else { - Log.w("ATag", "Error parsing A Tag. Pubkey is not hex: $aTagId") - null - } - } catch (t: Throwable) { - Log.w("ATag", "Error parsing A Tag: $aTagId: ${t.message}") - null - } + ) = ATagParser.parse(aTagId, relay) @JvmStatic - fun parse(tags: Array): ATag? { - require(tags[0] == TAG_NAME) - return parse(tags[1], tags.getOrNull(2)) - } + fun parse(tag: Array) = ATagParser.parse(TAG_NAME, tag) + + @JvmStatic + fun parseValidAddress(tag: Array) = ATagParser.parseValidAddress(TAG_NAME, tag) + + @JvmStatic + fun parseAddress(tag: Array) = ATagParser.parseAddress(TAG_NAME, tag) @JvmStatic fun assemble( aTagId: HexKey, relay: String?, - ) = arrayOfNotNull(TAG_NAME, aTagId, relay) + ) = ATagParser.assemble(TAG_NAME, aTagId, relay) @JvmStatic fun assemble( kind: Int, - pubKeyHex: String, + pubKey: String, dTag: String, relay: String?, - ) = arrayOfNotNull(TAG_NAME, assembleATagId(kind, pubKeyHex, dTag), relay) + ) = ATagParser.assemble(TAG_NAME, kind, pubKey, dTag, relay) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/ATagParser.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/ATagParser.kt new file mode 100644 index 000000000..9d8c64ffb --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/ATagParser.kt @@ -0,0 +1,123 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.tags.addressables + +import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.isNotName +import com.vitorpamplona.quartz.nip01Core.core.match +import com.vitorpamplona.quartz.utils.arrayOfNotNull + +class ATagParser { + companion object { + const val TAG_SIZE = 2 + + @JvmStatic + fun isTagged( + tag: Array, + tagName: String, + addressId: String, + ) = tag.match(tagName, addressId, TAG_SIZE) + + @JvmStatic + fun isIn( + tag: Array, + tagName: String, + addressIds: Set, + ) = tag.match(tagName, addressIds, TAG_SIZE) + + @JvmStatic + fun isTaggedWithKind( + tag: Array, + tagName: String, + kind: String, + ) = tag.match(tagName, TAG_SIZE) && Address.isOfKind(tag[1], kind) + + @JvmStatic + fun assembleATagId( + kind: Int, + pubKeyHex: HexKey, + dTag: String, + ) = Address.assemble(kind, pubKeyHex, dTag) + + @JvmStatic + fun parseIfOfKind( + tag: Array, + tagName: String, + kind: String, + ) = if (isTaggedWithKind(tag, tagName, kind)) parse(tag[1], tag.getOrNull(2)) else null + + @JvmStatic + fun parseIfIsIn( + tag: Array, + tagName: String, + addresses: Set, + ) = if (isIn(tag, tagName, addresses)) parse(tag[1], tag.getOrNull(2)) else null + + @JvmStatic + fun parse( + aTagId: String, + relay: String?, + ) = Address.parse(aTagId)?.let { ATag(it.kind, it.pubKeyHex, it.dTag, relay) } + + @JvmStatic + fun parse( + tagName: String, + tag: Array, + ): ATag? { + if (tag.isNotName(tagName, TAG_SIZE)) return null + return parse(tag[1], tag.getOrNull(2)) + } + + @JvmStatic + fun parseValidAddress( + tagName: String, + tag: Array, + ): String? { + if (tag.isNotName(tagName, TAG_SIZE)) return null + return Address.parse(tag[1])?.toValue() + } + + @JvmStatic + fun parseAddress( + tagName: String, + tag: Array, + ): String? { + if (tag.isNotName(tagName, TAG_SIZE)) return null + return tag[1] + } + + @JvmStatic + fun assemble( + tagName: String, + aTagId: HexKey, + relay: String?, + ) = arrayOfNotNull(tagName, aTagId, relay) + + @JvmStatic + fun assemble( + tagName: String, + kind: Int, + pubKeyHex: String, + dTag: String, + relay: String?, + ) = arrayOfNotNull(tagName, assembleATagId(kind, pubKeyHex, dTag), relay) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/Address.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/Address.kt new file mode 100644 index 000000000..0591f2ac3 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/Address.kt @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.tags.addressables + +import android.util.Log +import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.utils.Hex +import com.vitorpamplona.quartz.utils.bytesUsedInMemory +import com.vitorpamplona.quartz.utils.pointerSizeInBytes + +class Address( + val kind: Int, + val pubKeyHex: HexKey, + val dTag: String, +) { + fun toValue() = assemble(kind, pubKeyHex, dTag) + + fun countMemory(): Long = + 3 * pointerSizeInBytes + + 8L + // kind + pubKeyHex.bytesUsedInMemory() + + dTag.bytesUsedInMemory() + + companion object { + fun assemble( + kind: Int, + pubKeyHex: HexKey, + dTag: String, + ) = "$kind:$pubKeyHex:$dTag" + + @JvmStatic + fun parse(addressId: String): Address? = + try { + val parts = addressId.split(":", limit = 3) + if (parts[1].length == 64 && Hex.isHex(parts[1])) { + Address(parts[0].toInt(), parts[1], parts[2]) + } else { + Log.w("AddressableId", "Error parsing. Pubkey is not hex: $addressId") + throw RuntimeException("It shouldn't get here.") + null + } + } catch (t: Throwable) { + Log.e("AddressableId", "Error parsing: $addressId: ${t.message}", t) + null + } + + fun isOfKind( + addressId: String, + kind: String, + ) = addressId.startsWith(kind) && addressId[kind.length] == ':' + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/AddressReference.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/AddressReference.kt new file mode 100644 index 000000000..c5d504674 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/AddressReference.kt @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.tags.addressables + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.utils.bytesUsedInMemory +import com.vitorpamplona.quartz.utils.pointerSizeInBytes + +@Immutable +data class AddressReference( + val addressId: String, + var relay: String? = null, +) { + fun countMemory(): Long = + 2 * pointerSizeInBytes + // 7 fields, 4 bytes each reference (32bit) + addressId.bytesUsedInMemory() + + (relay?.bytesUsedInMemory() ?: 0) +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/TagArrayBuilderExt.kt new file mode 100644 index 000000000..d59535ad3 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/TagArrayBuilderExt.kt @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.tags.addressables + +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder + +fun TagArrayBuilder.aTag(tag: ATag) = add(tag.toATagArray()) + +fun TagArrayBuilder.aTags(tag: List) = addAll(tag.map { it.toATagArray() }) + +fun TagArrayBuilder.removeATag(tag: ATag) = this.removeIf(ATag::isSameAddress, tag.toATagArray()) + +fun TagArrayBuilder.qTag(tag: ATag) = add(tag.toQTagArray()) + +fun TagArrayBuilder.qTags(tag: List) = addAll(tag.map { it.toQTagArray() }) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/TagArrayExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/TagArrayExt.kt index 00da4d1d5..fe2fd46f0 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/TagArrayExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/TagArrayExt.kt @@ -21,40 +21,22 @@ package com.vitorpamplona.quartz.nip01Core.tags.addressables import com.vitorpamplona.quartz.nip01Core.core.TagArray -import com.vitorpamplona.quartz.nip01Core.core.firstMapTagged -import com.vitorpamplona.quartz.nip01Core.core.isAnyTagged -import com.vitorpamplona.quartz.nip01Core.core.isTagged -import com.vitorpamplona.quartz.nip01Core.core.mapTagged +import com.vitorpamplona.quartz.nip01Core.core.any +import com.vitorpamplona.quartz.nip01Core.core.firstNotNullOfOrNull import com.vitorpamplona.quartz.nip01Core.core.mapValueTagged -import com.vitorpamplona.quartz.nip19Bech32.parse fun TagArray.mapTaggedAddress(map: (address: String) -> R) = this.mapValueTagged(ATag.TAG_NAME, map) -fun TagArray.firstIsTaggedAddressableNote(addressableNotes: Set) = - this - .firstOrNull { it.size > 1 && it[0] == ATag.TAG_NAME && it[1] in addressableNotes } - ?.getOrNull(1) +fun TagArray.firstIsTaggedAddressableNote(addressableNotes: Set) = this.firstNotNullOfOrNull(ATag::parseIfIsIn, addressableNotes) -fun TagArray.isTaggedAddressableNote(idHex: String) = this.isTagged(ATag.TAG_NAME, idHex) +fun TagArray.isTaggedAddressableNote(idHex: String) = this.any(ATag::isTagged, idHex) -fun TagArray.isTaggedAddressableNotes(idHexes: Set) = this.isAnyTagged(ATag.TAG_NAME, idHexes) +fun TagArray.isTaggedAddressableNotes(idHexes: Set) = this.any(ATag::isIn, idHexes) -fun TagArray.isTaggedAddressableKind(kind: Int): Boolean { - val kindStr = kind.toString() - return this.any { it.size > 1 && it[0] == ATag.TAG_NAME && it[1].startsWith(kindStr) } -} +fun TagArray.isTaggedAddressableKind(kind: Int) = this.any(ATag::isTaggedWithKind, kind.toString()) -fun TagArray.getTagOfAddressableKind(kind: Int): ATag? { - val kindStr = kind.toString() - val aTag = - this - .firstOrNull { it.size > 1 && it[0] == ATag.TAG_NAME && it[1].startsWith(kindStr) } - ?.getOrNull(1) - ?: return null +fun TagArray.getTagOfAddressableKind(kind: Int) = this.firstNotNullOfOrNull(ATag::parseIfOfKind, kind.toString()) - return ATag.parse(aTag, null) -} +fun TagArray.taggedAddresses() = this.mapNotNull(ATag::parse) -fun TagArray.taggedAddresses() = this.mapTagged(ATag.TAG_NAME) { ATag.parse(it) } - -fun TagArray.firstTaggedAddress() = this.firstMapTagged(ATag.TAG_NAME) { ATag.parse(it) } +fun TagArray.firstTaggedAddress() = this.firstNotNullOfOrNull(ATag::parse) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/dTags/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/dTags/TagArrayBuilderExt.kt index 96fc5e73b..a85f6be7b 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/dTags/TagArrayBuilderExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/dTags/TagArrayBuilderExt.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.quartz.nip01Core.tags.dTags +import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder -fun TagArrayBuilder.dTag(name: String) = add(DTag.assemble(name)) +fun TagArrayBuilder.dTag(name: String) = addUnique(DTag.assemble(name)) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/events/ETag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/events/ETag.kt index 10d45bd70..c4c0b08fd 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/events/ETag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/events/ETag.kt @@ -29,35 +29,47 @@ import com.vitorpamplona.quartz.utils.pointerSizeInBytes @Immutable data class ETag( - val eventId: HexKey, -) { - var relay: String? = null - var authorPubKeyHex: HexKey? = null + override val eventId: HexKey, +) : GenericETag { + override var relay: String? = null + override var author: HexKey? = null constructor(eventId: HexKey, relayHint: String? = null, authorPubKeyHex: HexKey? = null) : this(eventId) { this.relay = relayHint - this.authorPubKeyHex = authorPubKeyHex + this.author = authorPubKeyHex } fun countMemory(): Long = 3 * pointerSizeInBytes + // 3 fields, 4 bytes each reference (32bit) eventId.bytesUsedInMemory() + (relay?.bytesUsedInMemory() ?: 0) + - (authorPubKeyHex?.bytesUsedInMemory() ?: 0) + (author?.bytesUsedInMemory() ?: 0) - fun toNEvent(): String = NEvent.create(eventId, authorPubKeyHex, null, relay) + fun toNEvent(): String = NEvent.create(eventId, author, null, relay) - fun toETagArray() = arrayOfNotNull(TAG_NAME, eventId, relay, authorPubKeyHex) + override fun toTagArray() = toNamedTagArray(TAG_NAME) - fun toQTagArray() = arrayOfNotNull("q", eventId, relay, authorPubKeyHex) + fun toQTagArray() = toNamedTagArray("q") + + fun toNamedTagArray(key: String) = arrayOfNotNull(key, eventId, relay, author) companion object { const val TAG_NAME = "e" + const val TAG_SIZE = 2 @JvmStatic - fun parse(tags: Array): ETag { - require(tags[0] == TAG_NAME) - return ETag(tags[1], tags.getOrNull(2), tags.getOrNull(3)) + fun isTagged(tag: Array) = tag.size >= TAG_SIZE && tag[0] == TAG_NAME && tag[1].isNotEmpty() + + @JvmStatic + fun parse(tag: Array): ETag? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return ETag(tag[1], tag.getOrNull(2), tag.getOrNull(3)) + } + + @JvmStatic + fun parseId(tag: Array): String? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1] } @JvmStatic diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/events/EventReference.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/events/EventReference.kt new file mode 100644 index 000000000..4196cd21e --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/events/EventReference.kt @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.tags.events + +import com.vitorpamplona.quartz.nip01Core.HexKey + +class EventReference( + val eventId: HexKey, + val author: HexKey?, + val relayHint: String?, +) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/events/GenericETag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/events/GenericETag.kt new file mode 100644 index 000000000..b8af54f12 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/events/GenericETag.kt @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.tags.events + +import com.vitorpamplona.quartz.nip01Core.HexKey + +interface GenericETag { + val eventId: HexKey + val relay: String? + val author: HexKey? + + fun toTagArray(): Array +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/events/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/events/TagArrayBuilderExt.kt new file mode 100644 index 000000000..260d464e1 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/events/TagArrayBuilderExt.kt @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.tags.events + +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder + +fun TagArrayBuilder.eTag(tag: ETag) = add(tag.toTagArray()) + +fun TagArrayBuilder.eTags(tag: List) = addAll(tag.map { it.toTagArray() }) + +fun TagArrayBuilder.qTag(tag: ETag) = add(tag.toQTagArray()) + +fun TagArrayBuilder.qTags(tag: List) = addAll(tag.map { it.toQTagArray() }) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/events/TagArrayExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/events/TagArrayExt.kt index 5010c44c2..b0ed27872 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/events/TagArrayExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/events/TagArrayExt.kt @@ -22,22 +22,18 @@ package com.vitorpamplona.quartz.nip01Core.tags.events import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArray -import com.vitorpamplona.quartz.nip01Core.core.firstMapTagged import com.vitorpamplona.quartz.nip01Core.core.forEachTagged import com.vitorpamplona.quartz.nip01Core.core.isTagged -import com.vitorpamplona.quartz.nip01Core.core.mapTagged import com.vitorpamplona.quartz.nip01Core.core.mapValueTagged -import com.vitorpamplona.quartz.nip01Core.core.mapValues -import com.vitorpamplona.quartz.nip19Bech32.parse fun TagArray.forEachTaggedEventId(onEach: (eventId: HexKey) -> Unit) = this.forEachTagged(ETag.TAG_NAME, onEach) fun TagArray.mapTaggedEventId(map: (eventId: HexKey) -> R) = this.mapValueTagged(ETag.TAG_NAME, map) -fun TagArray.taggedEvents() = this.mapTagged(ETag.TAG_NAME) { ETag.parse(it) } +fun TagArray.taggedEvents() = this.mapNotNull(ETag::parse) -fun TagArray.taggedEventIds() = this.mapValues(ETag.TAG_NAME) +fun TagArray.taggedEventIds() = this.mapNotNull(ETag::parseId) -fun TagArray.firstTaggedEvent() = this.firstMapTagged(ETag.TAG_NAME) { ETag.parse(it) } +fun TagArray.firstTaggedEvent() = this.firstNotNullOfOrNull(ETag::parse) fun TagArray.isTaggedEvent(idHex: String) = this.isTagged(ETag.TAG_NAME, idHex) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/geohash/GeoHash.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/geohash/GeoHash.kt index d4006c78a..650351ccd 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/geohash/GeoHash.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/geohash/GeoHash.kt @@ -22,10 +22,15 @@ package com.vitorpamplona.quartz.nip01Core.tags.geohash import com.vitorpamplona.quartz.nip01Core.core.TagArray -fun geohashMipMap(geohash: String): TagArray = - geohash.indices - .asSequence() - .map { arrayOf("g", geohash.substring(0, it + 1)) } - .toList() - .reversed() - .toTypedArray() +class GeoHash { + companion object { + const val TAG_NAME = "g" + + @JvmStatic + fun geoMipMap(geohash: String): List = geohash.indices.map { geohash.substring(0, it + 1) }.reversed() + + fun geohashMipMap(geohash: String): TagArray = geoMipMap(geohash).map { arrayOf(TAG_NAME, it) }.toTypedArray() + + fun assemble(geohash: String) = geohashMipMap(geohash) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/geohash/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/geohash/TagArrayBuilderExt.kt new file mode 100644 index 000000000..660315e32 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/geohash/TagArrayBuilderExt.kt @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.tags.geohash + +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder + +fun TagArrayBuilder.geohash(tag: String) = addAll(GeoHash.assemble(tag)) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/geohash/TagArrayExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/geohash/TagArrayExt.kt index a5aef9f3f..f4be57659 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/geohash/TagArrayExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/geohash/TagArrayExt.kt @@ -26,12 +26,12 @@ import com.vitorpamplona.quartz.nip01Core.core.hasTagWithContent import com.vitorpamplona.quartz.nip01Core.core.isAnyTagged import com.vitorpamplona.quartz.nip01Core.core.mapValues -fun TagArray.hasGeohashes() = this.hasTagWithContent("g") +fun TagArray.hasGeohashes() = this.hasTagWithContent(GeoHash.TAG_NAME) -fun TagArray.isTaggedGeoHashes(hashtags: Set) = this.isAnyTagged("g", hashtags) +fun TagArray.isTaggedGeoHashes(hashtags: Set) = this.isAnyTagged(GeoHash.TAG_NAME, hashtags) -fun TagArray.isTaggedGeoHash(hashtag: String) = this.anyTagWithValueStartingWith("g", hashtag) +fun TagArray.isTaggedGeoHash(hashtag: String) = this.anyTagWithValueStartingWith(GeoHash.TAG_NAME, hashtag) -fun TagArray.geohashes() = this.mapValues("g") +fun TagArray.geohashes() = this.mapValues(GeoHash.TAG_NAME) fun TagArray.getGeoHash(): String? = geohashes().maxByOrNull { it.length }?.ifBlank { null } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/hashtags/HashTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/hashtags/HashTag.kt new file mode 100644 index 000000000..68e46b97e --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/hashtags/HashTag.kt @@ -0,0 +1,62 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.tags.hashtags + +class HashTag { + companion object { + const val TAG_NAME = "t" + const val TAG_SIZE = 2 + + @JvmStatic + fun isTagged(tag: Array) = tag.size >= TAG_SIZE && tag[0] == TAG_NAME && tag[1].isNotEmpty() + + @JvmStatic + fun parse(tag: Array): String? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1] + } + + fun assemble(name: String) = arrayOf(TAG_NAME, name) + + fun assembleDualCase(name: String): List> { + val lowercaseTag = name.lowercase() + return if (name != lowercaseTag) { + listOf(assemble(name), assemble(lowercaseTag)) + } else { + listOf(assemble(name)) + } + } + + fun assemble(tags: List): List> { + val uniqueTags = mutableSetOf() + + tags.forEach { tag -> + uniqueTags.add(tag) + val lowercaseTag = tag.lowercase() + if (tag != lowercaseTag) { + uniqueTags.add(lowercaseTag) + } + } + + return uniqueTags.map { assemble(it) } + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/hashtags/HashTags.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/hashtags/TagArrayBuilderExt.kt similarity index 76% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/hashtags/HashTags.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/hashtags/TagArrayBuilderExt.kt index f2c124274..ec68e5877 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/hashtags/HashTags.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/hashtags/TagArrayBuilderExt.kt @@ -20,18 +20,9 @@ */ package com.vitorpamplona.quartz.nip01Core.tags.hashtags -fun buildHashtagTags(tags: List): List> { - val uniqueTags = mutableSetOf() +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder - tags.forEach { tag -> - uniqueTags.add(tag) - val lowercaseTag = tag.lowercase() - if (tag != lowercaseTag) { - uniqueTags.add(lowercaseTag) - } - } +fun TagArrayBuilder.hashtag(tag: String) = addAll(HashTag.assembleDualCase(tag)) - return uniqueTags.map { - arrayOf("t", it) - } -} +fun TagArrayBuilder.hashtags(tag: List) = addAll(HashTag.assemble(tag)) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/hashtags/TagArrayExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/hashtags/TagArrayExt.kt index 4509f2841..edcfc2562 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/hashtags/TagArrayExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/hashtags/TagArrayExt.kt @@ -23,20 +23,25 @@ package com.vitorpamplona.quartz.nip01Core.tags.hashtags import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArray import com.vitorpamplona.quartz.nip01Core.core.anyTagged +import com.vitorpamplona.quartz.nip01Core.core.firstAnyLowercaseTaggedValue import com.vitorpamplona.quartz.nip01Core.core.forEachTagged import com.vitorpamplona.quartz.nip01Core.core.hasTagWithContent +import com.vitorpamplona.quartz.nip01Core.core.isAnyLowercaseTagged +import com.vitorpamplona.quartz.nip01Core.core.isTagged import com.vitorpamplona.quartz.nip01Core.core.mapValues -fun TagArray.forEachHashTag(onEach: (eventId: HexKey) -> Unit) = this.forEachTagged("t", onEach) +fun TagArray.forEachHashTag(onEach: (eventId: HexKey) -> Unit) = this.forEachTagged(HashTag.TAG_NAME, onEach) -fun TagArray.anyHashTag(onEach: (str: String) -> Boolean) = this.anyTagged("t", onEach) +fun TagArray.anyHashTag(onEach: (str: String) -> Boolean) = this.anyTagged(HashTag.TAG_NAME, onEach) -fun TagArray.hasHashtags() = this.hasTagWithContent("t") +fun TagArray.hasHashtags() = this.hasTagWithContent(HashTag.TAG_NAME) -fun TagArray.hashtags() = this.mapValues("t") +fun TagArray.hashtags() = this.mapValues(HashTag.TAG_NAME) -fun TagArray.isTaggedHash(hashtag: String) = this.any { it.size > 1 && it[0] == "t" && it[1].equals(hashtag, true) } +fun TagArray.countHashtags() = this.count(HashTag::isTagged) -fun TagArray.isTaggedHashes(hashtags: Set) = this.any { it.size > 1 && it[0] == "t" && it[1].lowercase() in hashtags } +fun TagArray.isTaggedHash(hashtag: String) = this.isTagged(HashTag.TAG_NAME, hashtag, true) -fun TagArray.firstIsTaggedHashes(hashtags: Set) = this.firstOrNull { it.size > 1 && it[0] == "t" && it[1].lowercase() in hashtags }?.getOrNull(1) +fun TagArray.isTaggedHashes(hashtags: Set) = this.isAnyLowercaseTagged(HashTag.TAG_NAME, hashtags) + +fun TagArray.firstIsTaggedHashes(hashtags: Set) = this.firstAnyLowercaseTaggedValue(HashTag.TAG_NAME, hashtags) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/kinds/EventExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/kinds/EventExt.kt new file mode 100644 index 000000000..ff106533b --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/kinds/EventExt.kt @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.tags.kinds + +import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.Event + +fun Event.forEachKindTag(onEach: (eventId: HexKey) -> Unit) = tags.forEachKind(onEach) + +fun Event.anyKindTag(onEach: (str: String) -> Boolean) = tags.anyKind(onEach) + +fun Event.hasKindTag() = tags.hasKind() + +fun Event.kinds() = tags.kinds() + +fun Event.isTaggedKind(kind: Int) = tags.isTaggedKind(kind) + +fun Event.isTaggedKinds(kinds: Set) = tags.isTaggedKinds(kinds) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/kinds/KindTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/kinds/KindTag.kt new file mode 100644 index 000000000..051199464 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/kinds/KindTag.kt @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.tags.kinds + +class KindTag { + companion object { + const val TAG_NAME = "k" + const val TAG_SIZE = 2 + + fun match(tag: Array) = tag.size >= TAG_SIZE && tag[0] == TAG_NAME + + fun isTagged( + tag: Array, + kind: String, + ) = tag.size >= TAG_SIZE && tag[0] == TAG_NAME && tag[1] == kind + + fun isIn( + tag: Array, + kinds: Set, + ) = tag.size >= TAG_SIZE && tag[0] == TAG_NAME && tag[1] in kinds + + @JvmStatic + fun parse(tag: Array): Int? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1].toInt() + } + + fun assemble(kind: Int) = arrayOf(TAG_NAME, kind.toString()) + + fun assemble(kinds: List): List> = kinds.map { assemble(it) } + + fun assemble(kinds: Set): List> = kinds.map { assemble(it) } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/kinds/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/kinds/TagArrayBuilderExt.kt new file mode 100644 index 000000000..913f096f5 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/kinds/TagArrayBuilderExt.kt @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.tags.kinds + +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder + +fun TagArrayBuilder.kind(kind: Int) = add(KindTag.assemble(kind)) + +fun TagArrayBuilder.kinds(kinds: List) = addAll(KindTag.assemble(kinds)) + +fun TagArrayBuilder.kinds(kinds: Set) = addAll(KindTag.assemble(kinds)) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/kinds/TagArrayExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/kinds/TagArrayExt.kt new file mode 100644 index 000000000..ac6c05380 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/kinds/TagArrayExt.kt @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.tags.kinds + +import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArray +import com.vitorpamplona.quartz.nip01Core.core.any +import com.vitorpamplona.quartz.nip01Core.core.anyTagged +import com.vitorpamplona.quartz.nip01Core.core.forEachTagged + +fun TagArray.forEachKind(onEach: (eventId: HexKey) -> Unit) = this.forEachTagged(KindTag.TAG_NAME, onEach) + +fun TagArray.anyKind(onEach: (str: String) -> Boolean) = this.anyTagged(KindTag.TAG_NAME, onEach) + +fun TagArray.hasKind() = this.any(KindTag::match) + +fun TagArray.kinds() = this.mapNotNull(KindTag::parse) + +fun TagArray.isTaggedKind(kind: Int): Boolean = this.any(KindTag::isTagged, kind.toString()) + +fun TagArray.isTaggedKinds(kinds: Set) = this.any(KindTag::isIn, kinds.mapTo(HashSet()) { it.toString() }) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/people/EntityExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/people/EntityExt.kt new file mode 100644 index 000000000..d44a1e696 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/people/EntityExt.kt @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.tags.people + +import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile +import com.vitorpamplona.quartz.nip19Bech32.entities.NPub + +fun NProfile.toQuoteTag() = PTag(hex, relay.firstOrNull()) + +fun NPub.toQuoteTag() = PTag(hex, null) + +fun NProfile.toQuoteTagArray() = PTag.assemble(hex, relay.firstOrNull()) + +fun NPub.toQuoteTagArray() = PTag.assemble(hex, null) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/people/EventExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/people/EventExt.kt index a573de773..6248f9353 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/people/EventExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/people/EventExt.kt @@ -21,18 +21,17 @@ package com.vitorpamplona.quartz.nip01Core.tags.people import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.core.firstTagValue -import com.vitorpamplona.quartz.nip01Core.core.hasTagWithContent -import com.vitorpamplona.quartz.nip01Core.core.isAnyTagged -import com.vitorpamplona.quartz.nip01Core.core.isTagged -import com.vitorpamplona.quartz.nip01Core.core.mapValues -fun Event.isTaggedUser(idHex: String) = tags.isTagged("p", idHex) +fun Event.isTaggedUser(idHex: String) = tags.isTaggedUser(idHex) -fun Event.isTaggedUsers(idHexes: Set) = tags.isAnyTagged("p", idHexes) +fun Event.isTaggedUsers(idHexes: Set) = tags.isTaggedUsers(idHexes) -fun Event.taggedUsers() = tags.mapValues("p") +fun Event.taggedUsers() = tags.taggedUsers() -fun Event.firstTaggedUser() = tags.firstTagValue("p") +fun Event.taggedUserIds() = tags.taggedUserIds() -fun Event.hasAnyTaggedUser() = tags.hasTagWithContent("p") +fun Event.firstTaggedUser() = tags.firstTaggedUser() + +fun Event.firstTaggedUserId() = tags.firstTaggedUserId() + +fun Event.hasAnyTaggedUser() = tags.hasAnyTaggedUser() diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/people/PTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/people/PTag.kt new file mode 100644 index 000000000..3228a7b0a --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/people/PTag.kt @@ -0,0 +1,79 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.tags.people + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.Tag +import com.vitorpamplona.quartz.nip01Core.core.isNotName +import com.vitorpamplona.quartz.nip01Core.hexToByteArray +import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile +import com.vitorpamplona.quartz.nip19Bech32.toNpub +import com.vitorpamplona.quartz.utils.arrayOfNotNull +import com.vitorpamplona.quartz.utils.bytesUsedInMemory +import com.vitorpamplona.quartz.utils.pointerSizeInBytes + +@Immutable +data class PTag( + override val pubKey: HexKey, + override val relayHint: String? = null, +) : PubKeyReferenceTag { + fun countMemory(): Long = + 2 * pointerSizeInBytes + // 2 fields, 4 bytes each reference (32bit) + pubKey.bytesUsedInMemory() + + (relayHint?.bytesUsedInMemory() ?: 0) + + fun toNProfile(): String = NProfile.create(pubKey, relayHint?.let { listOf(it) } ?: emptyList()) + + fun toNPub(): String = pubKey.hexToByteArray().toNpub() + + fun toTagArray() = assemble(pubKey, relayHint) + + companion object { + const val TAG_NAME = "p" + const val TAG_SIZE = 2 + + fun isTagged( + tag: Array, + key: HexKey, + ): Boolean = tag.size >= 2 && tag[0] == TAG_NAME && tag[1] == key + + @JvmStatic + fun parse(tag: Tag): PTag? { + if (tag.isNotName(TAG_NAME, TAG_SIZE)) return null + if (tag[1].length != 64) return null + return PTag(tag[1], tag.getOrNull(2)) + } + + @JvmStatic + fun parseKey(tag: Array): HexKey? { + if (tag.isNotName(TAG_NAME, TAG_SIZE)) return null + if (tag[1].length != 64) return null + return tag[1] + } + + @JvmStatic + fun assemble( + pubkey: HexKey, + relayHint: String?, + ) = arrayOfNotNull(TAG_NAME, pubkey, relayHint) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip36SensitiveContent/ContentWarningSerializer.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/people/PubKeyReferenceTag.kt similarity index 85% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip36SensitiveContent/ContentWarningSerializer.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/people/PubKeyReferenceTag.kt index f04756dd1..4fb4bdb6d 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip36SensitiveContent/ContentWarningSerializer.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/people/PubKeyReferenceTag.kt @@ -18,10 +18,11 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip36SensitiveContent +package com.vitorpamplona.quartz.nip01Core.tags.people -class ContentWarningSerializer { - companion object { - fun toTagArray(reason: String = "") = arrayOf(CONTENT_WARNING, reason) - } +import com.vitorpamplona.quartz.nip01Core.HexKey + +interface PubKeyReferenceTag { + val pubKey: HexKey + val relayHint: String? } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/people/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/people/TagArrayBuilderExt.kt new file mode 100644 index 000000000..8e3e7c9ac --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/people/TagArrayBuilderExt.kt @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.tags.people + +import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder + +fun TagArrayBuilder.pTag( + pubkey: HexKey, + relayHint: String? = null, +) = add(PTag.assemble(pubkey, relayHint)) + +fun TagArrayBuilder.pTagIds(tag: Set) = addAll(tag.map { PTag.assemble(it, null) }) + +fun TagArrayBuilder.pTag(tag: PTag) = add(tag.toTagArray()) + +fun TagArrayBuilder.pTags(tag: List) = addAll(tag.map { it.toTagArray() }) + +fun TagArrayBuilder.pTags(tag: Set) = addAll(tag.map { it.toTagArray() }) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/people/TagArrayExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/people/TagArrayExt.kt index c21f06b52..e8d099ba9 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/people/TagArrayExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/people/TagArrayExt.kt @@ -21,18 +21,21 @@ package com.vitorpamplona.quartz.nip01Core.tags.people import com.vitorpamplona.quartz.nip01Core.core.TagArray -import com.vitorpamplona.quartz.nip01Core.core.firstTagValue import com.vitorpamplona.quartz.nip01Core.core.hasTagWithContent import com.vitorpamplona.quartz.nip01Core.core.isAnyTagged import com.vitorpamplona.quartz.nip01Core.core.isTagged -import com.vitorpamplona.quartz.nip01Core.core.mapValues +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag.Companion.TAG_NAME -fun TagArray.isTaggedUser(idHex: String) = this.isTagged("p", idHex) +fun TagArray.isTaggedUser(idHex: String) = this.isTagged(TAG_NAME, idHex) -fun TagArray.isTaggedUsers(idHexes: Set) = this.isAnyTagged("p", idHexes) +fun TagArray.isTaggedUsers(idHexes: Set) = this.isAnyTagged(TAG_NAME, idHexes) -fun TagArray.taggedUsers() = this.mapValues("p") +fun TagArray.taggedUsers() = this.mapNotNull(PTag::parse) -fun TagArray.firstTaggedUser() = this.firstTagValue("p") +fun TagArray.firstTaggedUser() = this.firstNotNullOfOrNull(PTag::parse) -fun TagArray.hasAnyTaggedUser() = this.hasTagWithContent("p") +fun TagArray.taggedUserIds() = this.mapNotNull(PTag::parseKey) + +fun TagArray.firstTaggedUserId() = this.firstNotNullOfOrNull(PTag::parseKey) + +fun TagArray.hasAnyTaggedUser() = this.hasTagWithContent(TAG_NAME) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/references/EventExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/references/EventExt.kt new file mode 100644 index 000000000..a14d9296e --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/references/EventExt.kt @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.tags.references + +import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.Event + +fun Event.forEachReferenceTag(onEach: (eventId: HexKey) -> Unit) = tags.forEachReference(onEach) + +fun Event.anyReferenceTag(onEach: (str: String) -> Boolean) = tags.anyReference(onEach) + +fun Event.hasReferenceTag() = tags.hasReferences() + +fun Event.references() = tags.references() + +fun Event.isTaggedReference(hashtag: String) = tags.isTaggedReference(hashtag) + +fun Event.isTaggedReferences(hashtags: Set) = tags.isTaggedReferences(hashtags) + +fun Event.firstIsTaggedReferences(hashtags: Set) = tags.firstIsTaggedReferences(hashtags) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/references/ReferenceTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/references/ReferenceTag.kt new file mode 100644 index 000000000..a60167807 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/references/ReferenceTag.kt @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.tags.references + +import com.vitorpamplona.quartz.nip96FileStorage.HttpUrlFormatter + +class ReferenceTag { + companion object { + const val TAG_NAME = "r" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): String? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1] + } + + fun assemble(url: String) = arrayOf(TAG_NAME, HttpUrlFormatter.normalize(url)) + + fun assemble(urls: List): List> = urls.mapTo(HashSet()) { HttpUrlFormatter.normalize(it) }.map { arrayOf(TAG_NAME, it) } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip96FileStorage/AuthToken.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/references/TagArrayBuilderExt.kt similarity index 75% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip96FileStorage/AuthToken.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/references/TagArrayBuilderExt.kt index d0a0d6de3..ae58cf4ca 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip96FileStorage/AuthToken.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/references/TagArrayBuilderExt.kt @@ -18,14 +18,11 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip96FileStorage +package com.vitorpamplona.quartz.nip01Core.tags.references -import com.vitorpamplona.quartz.nip98HttpAuth.HTTPAuthorizationEvent -import java.util.Base64 +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder -class AuthToken { - fun encodeAuth(event: HTTPAuthorizationEvent): String { - val encodedNIP98Event = Base64.getEncoder().encodeToString(event.toJson().toByteArray()) - return "Nostr $encodedNIP98Event" - } -} +fun TagArrayBuilder.reference(url: String) = add(ReferenceTag.assemble(url)) + +fun TagArrayBuilder.references(urls: List) = addAll(ReferenceTag.assemble(urls)) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/references/TagArrayExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/references/TagArrayExt.kt new file mode 100644 index 000000000..c7c74d35a --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/references/TagArrayExt.kt @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.tags.references + +import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArray +import com.vitorpamplona.quartz.nip01Core.core.anyTagged +import com.vitorpamplona.quartz.nip01Core.core.firstAnyLowercaseTaggedValue +import com.vitorpamplona.quartz.nip01Core.core.forEachTagged +import com.vitorpamplona.quartz.nip01Core.core.hasTagWithContent +import com.vitorpamplona.quartz.nip01Core.core.isAnyLowercaseTagged +import com.vitorpamplona.quartz.nip01Core.core.isTagged +import com.vitorpamplona.quartz.nip01Core.core.mapValues + +fun TagArray.forEachReference(onEach: (eventId: HexKey) -> Unit) = this.forEachTagged(ReferenceTag.TAG_NAME, onEach) + +fun TagArray.anyReference(onEach: (str: String) -> Boolean) = this.anyTagged(ReferenceTag.TAG_NAME, onEach) + +fun TagArray.hasReferences() = this.hasTagWithContent(ReferenceTag.TAG_NAME) + +fun TagArray.references() = this.mapValues(ReferenceTag.TAG_NAME) + +fun TagArray.isTaggedReference(hashtag: String) = this.isTagged(ReferenceTag.TAG_NAME, hashtag, true) + +fun TagArray.isTaggedReferences(hashtags: Set) = this.isAnyLowercaseTagged(ReferenceTag.TAG_NAME, hashtags) + +fun TagArray.firstIsTaggedReferences(hashtags: Set) = this.firstAnyLowercaseTaggedValue(ReferenceTag.TAG_NAME, hashtags) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/ContactListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/ContactListEvent.kt index 293170f69..5364cb093 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/ContactListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/ContactListEvent.kt @@ -20,33 +20,23 @@ */ package com.vitorpamplona.quartz.nip02FollowList -import android.util.Log -import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable -import com.fasterxml.jackson.module.kotlin.readValue import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.addressables.isTaggedAddressableNote import com.vitorpamplona.quartz.nip01Core.tags.events.isTaggedEvent import com.vitorpamplona.quartz.nip01Core.tags.geohash.isTaggedGeoHash +import com.vitorpamplona.quartz.nip01Core.tags.hashtags.countHashtags import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags import com.vitorpamplona.quartz.nip01Core.tags.hashtags.isTaggedHash +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser -import com.vitorpamplona.quartz.nip01Core.toHexKey -import com.vitorpamplona.quartz.nip19Bech32.decodePublicKey -import com.vitorpamplona.quartz.nip19Bech32.parse -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer +import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.utils.TimeUtils -@Immutable data class Contact( - val pubKeyHex: String, - val relayUri: String?, -) - @Stable class ContactListEvent( id: HexKey, @@ -59,66 +49,24 @@ class ContactListEvent( /** * Returns a list of p-tags that are verified as hex keys. */ - fun verifiedFollowKeySet(): Set = - tags.mapNotNullTo(mutableSetOf()) { - if (it.size > 1 && it[0] == "p") { - try { - decodePublicKey(it[1]).toHexKey() - } catch (e: Exception) { - Log.w("ContactListEvent", "Can't parse p-tag $it in the contact list of $pubKey with id $id", e) - null - } - } else { - null - } - } + fun verifiedFollowKeySet(): Set = tags.mapNotNullTo(HashSet(), ContactTag::parseValidKey) /** * Returns a list of a-tags that are verified as correct. */ - fun verifiedFollowAddressSet(): Set = - tags - .mapNotNullTo(mutableSetOf()) { - if (it.size > 1 && it[0] == "a") { - ATag.parse(it[1], null)?.toTag() - } else { - null - } - } + fun verifiedFollowAddressSet(): Set = tags.mapNotNullTo(HashSet(), ATag::parseValidAddress) - fun unverifiedFollowKeySet() = tags.filter { it.size > 1 && it[0] == "p" }.mapNotNull { it.getOrNull(1) } + fun unverifiedFollowKeySet() = tags.mapNotNull(PTag::parseKey) - fun unverifiedFollowTagSet() = tags.filter { it.size > 1 && it[0] == "t" }.mapNotNull { it.getOrNull(1) } + fun unverifiedFollowTagSet() = tags.hashtags() - fun countFollowTags() = tags.count { it.size > 1 && it[0] == "t" } + fun countFollowTags() = tags.countHashtags() - fun follows() = - tags.mapNotNull { - try { - if (it.size > 1 && it[0] == "p") { - Contact(decodePublicKey(it[1]).toHexKey(), it.getOrNull(2)) - } else { - null - } - } catch (e: Exception) { - Log.w("ContactListEvent", "Can't parse tags as a follows: ${it[1]}", e) - null - } - } + fun follows() = tags.mapNotNull(ContactTag::parseValid) fun followsTags() = hashtags() - fun relays(): Map? = - try { - if (content.isNotEmpty()) { - EventMapper.mapper.readValue>(content) - } else { - null - } - } catch (e: Exception) { - Log.w("ContactListEvent", "Can't parse content as relay lists: $content", e) - null - } + fun relays(): Map? = RelaySet.parse(content) companion object { const val KIND = 3 @@ -127,7 +75,7 @@ class ContactListEvent( fun blockListFor(pubKeyHex: HexKey): String = "3:$pubKeyHex:" fun createFromScratch( - followUsers: List = emptyList(), + followUsers: List = emptyList(), followTags: List = emptyList(), followGeohashes: List = emptyList(), followCommunities: List = emptyList(), @@ -136,18 +84,11 @@ class ContactListEvent( signer: NostrSignerSync, createdAt: Long = TimeUtils.now(), ): ContactListEvent? { - val content = - if (relayUse != null) { - EventMapper.mapper.writeValueAsString(relayUse) - } else { - "" - } + val content = relayUse?.let { RelaySet.assemble(it) } ?: "" val tags = - listOf(AltTagSerializer.toTagArray(ALT)) + - followUsers.map { - listOfNotNull("p", it.pubKeyHex, it.relayUri).toTypedArray() - } + + listOf(AltTag.assemble(ALT)) + + followUsers.map { it.toTagArray() } + followTags.map { arrayOf("t", it) } + followEvents.map { arrayOf("e", it) } + followCommunities.map { it.toATagArray() } + @@ -157,7 +98,7 @@ class ContactListEvent( } fun createFromScratch( - followUsers: List, + followUsers: List, followTags: List, followGeohashes: List, followCommunities: List, @@ -167,21 +108,10 @@ class ContactListEvent( createdAt: Long = TimeUtils.now(), onReady: (ContactListEvent) -> Unit, ) { - val content = - if (relayUse != null) { - EventMapper.mapper.writeValueAsString(relayUse) - } else { - "" - } + val content = relayUse?.let { RelaySet.assemble(it) } ?: "" val tags = - followUsers.map { - if (it.relayUri != null) { - arrayOf("p", it.pubKeyHex, it.relayUri) - } else { - arrayOf("p", it.pubKeyHex) - } - } + + followUsers.map { it.toTagArray() } + followTags.map { arrayOf("t", it) } + followEvents.map { arrayOf("e", it) } + followCommunities.map { it.toATagArray() } + @@ -387,12 +317,7 @@ class ContactListEvent( createdAt: Long = TimeUtils.now(), onReady: (ContactListEvent) -> Unit, ) { - val content = - if (relayUse != null) { - EventMapper.mapper.writeValueAsString(relayUse) - } else { - "" - } + val content = relayUse?.let { RelaySet.assemble(it) } ?: "" return create( content = content, @@ -414,23 +339,10 @@ class ContactListEvent( if (tags.any { it.size > 1 && it[0] == "alt" }) { tags } else { - tags + AltTagSerializer.toTagArray(ALT) + tags + AltTag.assemble(ALT) } signer.sign(createdAt, KIND, newTags, content, onReady) } } - - data class ReadWrite( - val read: Boolean, - val write: Boolean, - ) } - -@Stable class ImmutableListOfLists( - val lists: Array>, -) - -val EmptyTagList = ImmutableListOfLists(emptyArray()) - -fun Array>.toImmutableListOfLists(): ImmutableListOfLists = ImmutableListOfLists(this) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/ContactTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/ContactTag.kt new file mode 100644 index 000000000..14b46d196 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/ContactTag.kt @@ -0,0 +1,98 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip02FollowList + +import android.util.Log +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.toHexKey +import com.vitorpamplona.quartz.nip19Bech32.decodePublicKey +import com.vitorpamplona.quartz.utils.arrayOfNotNull +import com.vitorpamplona.quartz.utils.bytesUsedInMemory +import com.vitorpamplona.quartz.utils.pointerSizeInBytes + +@Immutable +data class ContactTag( + val pubKey: HexKey, +) { + var relayUri: String? = null + var petname: String? = null + + constructor( + pubKey: HexKey, + relayHint: String?, + petname: String?, + ) : this(pubKey) { + this.relayUri = relayHint + this.petname = petname + } + + fun countMemory(): Long = + 3 * pointerSizeInBytes + + pubKey.bytesUsedInMemory() + + (relayUri?.bytesUsedInMemory() ?: 0) + + (petname?.bytesUsedInMemory() ?: 0) + + fun toTagArray() = assemble(pubKey, relayUri, petname) + + companion object { + const val TAG_NAME = "p" + const val TAG_SIZE = 2 + + @JvmStatic + fun isTagged(tag: Array) = tag.size >= TAG_SIZE && tag[0] == TAG_NAME && tag[1].isNotEmpty() + + @JvmStatic + fun parse(tag: Array): ContactTag? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return ContactTag(tag[1], tag.getOrNull(2), tag.getOrNull(3)) + } + + @JvmStatic + fun parseValid(tag: Array): ContactTag? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return try { + ContactTag(decodePublicKey(tag[1]).toHexKey(), tag.getOrNull(2), tag.getOrNull(3)) + } catch (e: Exception) { + Log.w("ContactTag", "Can't parse contact list p-tag ${tag.joinToString(", ")}", e) + null + } + } + + @JvmStatic + fun parseValidKey(tag: Array): String? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return try { + decodePublicKey(tag[1]).toHexKey() + } catch (e: Exception) { + Log.w("ContactListEvent", "Can't parse contact list pubkey ${tag.joinToString(", ")}", e) + null + } + } + + @JvmStatic + fun assemble( + pubkey: HexKey, + relayUri: String? = null, + petname: String? = null, + ) = arrayOfNotNull(TAG_NAME, pubkey, relayUri, petname) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/ImmutableListOfLists.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/ImmutableListOfLists.kt new file mode 100644 index 000000000..513481ad0 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/ImmutableListOfLists.kt @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip02FollowList + +import androidx.compose.runtime.Stable + +@Stable +class ImmutableListOfLists( + val lists: Array>, +) + +val EmptyTagList = ImmutableListOfLists(emptyArray()) + +fun Array>.toImmutableListOfLists(): ImmutableListOfLists = ImmutableListOfLists(this) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/RelaySet.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/RelaySet.kt new file mode 100644 index 000000000..f25233d44 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/RelaySet.kt @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip02FollowList + +import android.util.Log +import com.fasterxml.jackson.module.kotlin.readValue +import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper + +data class ReadWrite( + val read: Boolean, + val write: Boolean, +) + +class RelaySet { + companion object { + fun assemble(relayUse: Map): String = EventMapper.mapper.writeValueAsString(relayUse) + + fun parse(content: String): Map? = + try { + if (content.isNotEmpty()) { + EventMapper.mapper.readValue>(content) + } else { + null + } + } catch (e: Exception) { + Log.w("ContactListEvent", "Can't parse content as relay lists: $content", e) + null + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/OtsEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/OtsEvent.kt index bfac829ec..3d55455b9 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/OtsEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/OtsEvent.kt @@ -20,24 +20,18 @@ */ package com.vitorpamplona.quartz.nip03Timestamp -import android.util.Log import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.EventHintBundle import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.hexToByteArray -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip03Timestamp.ots.BlockstreamExplorer -import com.vitorpamplona.quartz.nip03Timestamp.ots.CalendarPureJavaBuilder -import com.vitorpamplona.quartz.nip03Timestamp.ots.DetachedTimestampFile -import com.vitorpamplona.quartz.nip03Timestamp.ots.Hash -import com.vitorpamplona.quartz.nip03Timestamp.ots.OpenTimestamps -import com.vitorpamplona.quartz.nip03Timestamp.ots.VerifyResult -import com.vitorpamplona.quartz.nip03Timestamp.ots.exceptions.UrlException -import com.vitorpamplona.quartz.nip03Timestamp.ots.op.OpSHA256 -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag +import com.vitorpamplona.quartz.nip01Core.tags.events.eTag +import com.vitorpamplona.quartz.nip01Core.tags.kinds.kind +import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.utils.TimeUtils -import com.vitorpamplona.quartz.utils.pointerSizeInBytes -import kotlinx.coroutines.CancellationException import java.util.Base64 @Immutable @@ -49,139 +43,62 @@ class OtsEvent( content: String, sig: HexKey, ) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { - @Transient - var verification: VerificationState = VerificationState.NotStarted - - override fun countMemory(): Long = - super.countMemory() + - pointerSizeInBytes + Long.SIZE_BYTES // verifiedTime - override fun isContentEncoded() = true - fun digestEvent() = tags.firstOrNull { it.size > 1 && it[0] == "e" }?.get(1) + fun digestEventId() = tags.firstNotNullOfOrNull(ETag::parseId) - fun digest() = digestEvent()?.hexToByteArray() + fun otsByteArray(): ByteArray = decodeOtsState(content) - fun otsByteArray(): ByteArray = Base64.getDecoder().decode(content) + fun cacheVerify(): VerificationState = VerificationStateCache.cacheVerify(this) - fun cacheVerify(): VerificationState = - when (val verif = verification) { - is VerificationState.Verified -> verif - is VerificationState.NotStarted -> verifyState().also { verification = it } - is VerificationState.NetworkError -> { - // try again in 5 mins - if (verif.time < TimeUtils.fiveMinutesAgo()) { - verifyState().also { verification = it } - } else { - verif - } - } - is VerificationState.Error -> verif - } - - fun verifyState(): VerificationState = digestEvent()?.let { verify(otsByteArray(), it) } ?: VerificationState.Error("Digest Not found") + fun verifyState(): VerificationState = digestEventId()?.let { verify(otsByteArray(), it) } ?: VerificationState.Error("Digest Not found") fun verify(): Long? = (verifyState() as? VerificationState.Verified)?.verifiedTime - fun info(): String { - val detachedOts = DetachedTimestampFile.deserialize(otsByteArray()) - return otsInstance.info(detachedOts) - } - companion object { const val KIND = 1040 const val ALT = "Opentimestamps Attestation" - var otsInstance = - OpenTimestamps( - BlockstreamExplorer(), - CalendarPureJavaBuilder(), - ) - - fun stamp(eventId: HexKey): String { - val hash = - Hash( - eventId.hexToByteArray(), - OpSHA256._TAG, - ) - val file = DetachedTimestampFile.from(hash) - val timestamp = otsInstance.stamp(file) - val detachedToSerialize = - DetachedTimestampFile( - hash.getOp(), - timestamp, - ) - return Base64.getEncoder().encodeToString(detachedToSerialize.serialize()) - } + fun stamp(eventId: HexKey) = OtsResolver.stamp(eventId.hexToByteArray()) fun upgrade( - otsFile: String, + otsState: ByteArray, eventId: HexKey, - ): String { - val detachedOts = DetachedTimestampFile.deserialize(Base64.getDecoder().decode(otsFile)) - - return if (otsInstance.upgrade(detachedOts)) { - // if the change is now verifiable. - if (verify(detachedOts, eventId) is VerificationState.Verified) { - Base64.getEncoder().encodeToString(detachedOts.serialize()) - } else { - otsFile - } - } else { - otsFile - } - } + ) = OtsResolver.upgrade(otsState, eventId.hexToByteArray()) fun verify( - otsFile: String, + otsState: ByteArray, eventId: HexKey, - ): VerificationState = verify(Base64.getDecoder().decode(otsFile), eventId) + ) = OtsResolver.verify(otsState, eventId.hexToByteArray()) - fun verify( - otsFile: ByteArray, - eventId: HexKey, - ): VerificationState = verify(DetachedTimestampFile.deserialize(otsFile), eventId) + fun encodeOtsState(otsState: ByteArray) = Base64.getEncoder().encodeToString(otsState) - fun verify( - detachedOts: DetachedTimestampFile, - eventId: HexKey, - ): VerificationState { - try { - val result = otsInstance.verify(detachedOts, eventId.hexToByteArray()) - if (result == null || result.isEmpty()) { - return VerificationState.Error("Verification hashmap is empty") - } else { - val time = result.get(VerifyResult.Chains.BITCOIN)?.timestamp - return if (time != null) { - VerificationState.Verified(time) - } else { - VerificationState.Error("Does not include a Bitcoin verification") - } - } - } catch (e: Exception) { - if (e is CancellationException) throw e - Log.e("OpenTimeStamps", "Failed to verify", e) - return if (e is UrlException) { - VerificationState.NetworkError(e.message ?: e.cause?.message ?: "Failed to verify") - } else { - VerificationState.Error(e.message ?: e.cause?.message ?: "Failed to verify") - } - } - } + fun decodeOtsState(content: String) = Base64.getDecoder().decode(content) - fun create( + fun build( eventId: HexKey, - otsFileBase64: String, - signer: NostrSigner, + otsState: ByteArray, createdAt: Long = TimeUtils.now(), - onReady: (OtsEvent) -> Unit, - ) { - val tags = - arrayOf( - arrayOf("e", eventId), - AltTagSerializer.toTagArray(ALT), - ) - signer.sign(createdAt, KIND, tags, otsFileBase64, onReady) + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, encodeOtsState(otsState), createdAt) { + alt(ALT) + eTag(ETag(eventId)) + + initializer() + } + + fun build( + event: EventHintBundle, + otsState: ByteArray, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, encodeOtsState(otsState), createdAt) { + alt(ALT) + + eTag(event.toETag()) + kind(event.event.kind) + + initializer() } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/OtsResolver.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/OtsResolver.kt new file mode 100644 index 000000000..384c46e59 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/OtsResolver.kt @@ -0,0 +1,101 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip03Timestamp + +import android.util.Log +import com.vitorpamplona.quartz.nip03Timestamp.ots.BlockstreamExplorer +import com.vitorpamplona.quartz.nip03Timestamp.ots.CalendarPureJavaBuilder +import com.vitorpamplona.quartz.nip03Timestamp.ots.DetachedTimestampFile +import com.vitorpamplona.quartz.nip03Timestamp.ots.Hash +import com.vitorpamplona.quartz.nip03Timestamp.ots.OpenTimestamps +import com.vitorpamplona.quartz.nip03Timestamp.ots.VerifyResult +import com.vitorpamplona.quartz.nip03Timestamp.ots.exceptions.UrlException +import com.vitorpamplona.quartz.nip03Timestamp.ots.op.OpSHA256 +import kotlinx.coroutines.CancellationException + +object OtsResolver { + // default config + var ots = + OpenTimestamps( + BlockstreamExplorer(), + CalendarPureJavaBuilder(), + ) + + fun info(otsState: ByteArray): String = ots.info(DetachedTimestampFile.deserialize(otsState)) + + fun stamp(data: ByteArray): ByteArray { + val hash = Hash(data, OpSHA256._TAG) + val file = DetachedTimestampFile.from(hash) + val timestamp = ots.stamp(file) + val detachedToSerialize = DetachedTimestampFile(hash.getOp(), timestamp) + return detachedToSerialize.serialize() + } + + fun upgrade( + otsState: ByteArray, + data: ByteArray, + ): ByteArray? { + val detachedOts = DetachedTimestampFile.deserialize(otsState) + + return if (ots.upgrade(detachedOts)) { + // if the change is now verifiable. + if (verify(detachedOts, data) is VerificationState.Verified) { + detachedOts.serialize() + } else { + null + } + } else { + null + } + } + + fun verify( + otsFile: ByteArray, + data: ByteArray, + ): VerificationState = verify(DetachedTimestampFile.deserialize(otsFile), data) + + fun verify( + detachedOts: DetachedTimestampFile, + data: ByteArray, + ): VerificationState { + try { + val result = ots.verify(detachedOts, data) + if (result == null || result.isEmpty()) { + return VerificationState.Error("Verification hashmap is empty") + } else { + val time = result.get(VerifyResult.Chains.BITCOIN)?.timestamp + return if (time != null) { + VerificationState.Verified(time) + } else { + VerificationState.Error("Does not include a Bitcoin verification") + } + } + } catch (e: Exception) { + if (e is CancellationException) throw e + Log.e("OpenTimeStamps", "Failed to verify", e) + return if (e is UrlException) { + VerificationState.NetworkError(e.message ?: e.cause?.message ?: "Failed to verify") + } else { + VerificationState.Error(e.message ?: e.cause?.message ?: "Failed to verify") + } + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/VerificationStateCache.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/VerificationStateCache.kt new file mode 100644 index 000000000..625e993db --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/VerificationStateCache.kt @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip03Timestamp + +import android.util.LruCache +import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.utils.TimeUtils + +object VerificationStateCache { + private val cache = LruCache(200) + + fun cacheVerify(event: OtsEvent): VerificationState = + when (val verif = cache[event.id]) { + is VerificationState.Verified -> verif + is VerificationState.NetworkError -> { + // try again in 5 mins + if (verif.time < TimeUtils.fiveMinutesAgo()) { + event.verifyState().also { cache.put(event.id, it) } + } else { + verif + } + } + is VerificationState.Error -> verif + else -> event.verifyState().also { cache.put(event.id, it) } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/Nip04Encoder.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/Nip04Encoder.kt new file mode 100644 index 000000000..96fbfe922 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/Nip04Encoder.kt @@ -0,0 +1,78 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip04Dm + +import android.util.Log +import java.util.Base64 + +class Nip04Encoder( + val ciphertext: ByteArray, + val nonce: ByteArray, +) { + companion object { + const val V: Int = 0 + + fun decodePayload(payload: String): Nip04Encoder? { + return try { + val byteArray = Base64.getDecoder().decode(payload) + check(byteArray[0].toInt() == V) + return Nip04Encoder( + nonce = byteArray.copyOfRange(1, 25), + ciphertext = byteArray.copyOfRange(25, byteArray.size), + ) + } catch (e: Exception) { + Log.w("NIP04", "Unable to Parse encrypted payload: $payload") + null + } + } + + fun isNIP04(encoded: String): Boolean { + // cleaning up some bug from some client. + val cleanedUp = encoded.removeSuffix("-null") + + val l = cleanedUp.length + if (l < 28) return false + return cleanedUp[l - 28] == '?' && + cleanedUp[l - 27] == 'i' && + cleanedUp[l - 26] == 'v' && + cleanedUp[l - 25] == '=' + } + + fun decodeFromNIP04(payload: String): Nip04Encoder? = + try { + // cleaning up some bug from some client. + val parts = payload.removeSuffix("-null").split("?iv=") + Nip04Encoder( + ciphertext = Base64.getDecoder().decode(parts[0]), + nonce = Base64.getDecoder().decode(parts[1]), + ) + } catch (e: Exception) { + Log.w("NIP04", "Unable to Parse encrypted payload: $payload") + null + } + } + + fun encodeToNIP04(): String { + val nonce = Base64.getEncoder().encodeToString(nonce) + val ciphertext = Base64.getEncoder().encodeToString(ciphertext) + return "$ciphertext?iv=$nonce" + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/Nip04.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/Nip04Encryption.kt similarity index 60% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/Nip04.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/Nip04Encryption.kt index de2fa6a48..f02b44eeb 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/Nip04.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/Nip04Encryption.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.quartz.nip04Dm -import android.util.Log import com.vitorpamplona.quartz.nip44Encryption.SharedKeyCache import com.vitorpamplona.quartz.utils.Hex import fr.acinq.secp256k1.Secp256k1 @@ -30,7 +29,7 @@ import javax.crypto.Cipher import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.SecretKeySpec -class Nip04( +class Nip04Encryption( val secp256k1: Secp256k1, val random: SecureRandom, ) { @@ -50,16 +49,14 @@ class Nip04( fun encrypt( msg: String, sharedSecret: ByteArray, - ): EncryptedInfo { + ): Nip04Encoder { val iv = ByteArray(16) random.nextBytes(iv) val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(sharedSecret, "AES"), IvParameterSpec(iv)) - // val ivBase64 = Base64.getEncoder().encodeToString(iv) val encryptedMsg = cipher.doFinal(msg.toByteArray()) - // val encryptedMsgBase64 = Base64.getEncoder().encodeToString(encryptedMsg) - return EncryptedInfo(encryptedMsg, iv) + return Nip04Encoder(encryptedMsg, iv) } fun decrypt( @@ -72,7 +69,7 @@ class Nip04( } fun decrypt( - encryptedInfo: EncryptedInfo, + encryptedInfo: Nip04Encoder, privateKey: ByteArray, pubKey: ByteArray, ): String { @@ -84,7 +81,7 @@ class Nip04( msg: String, sharedSecret: ByteArray, ): String { - val decoded = EncryptedInfo.decodeFromNIP04(msg) + val decoded = Nip04Encoder.decodeFromNIP04(msg) check(decoded != null) { "Unable to decode msg $msg as NIP04" } return decrypt(decoded.ciphertext, decoded.nonce, sharedSecret) } @@ -128,67 +125,6 @@ class Nip04( ): ByteArray = secp256k1.pubKeyTweakMul(h02 + pubKey, privateKey).copyOfRange(1, 33) companion object { - fun isNIP04(encoded: String) = EncryptedInfo.isNIP04(encoded) - } - - class EncryptedInfo( - val ciphertext: ByteArray, - val nonce: ByteArray, - ) { - companion object { - const val V: Int = 0 - - fun decodePayload(payload: String): EncryptedInfo? { - return try { - val byteArray = Base64.getDecoder().decode(payload) - check(byteArray[0].toInt() == V) - return EncryptedInfo( - nonce = byteArray.copyOfRange(1, 25), - ciphertext = byteArray.copyOfRange(25, byteArray.size), - ) - } catch (e: Exception) { - Log.w("NIP04", "Unable to Parse encrypted payload: $payload") - null - } - } - - fun isNIP04(encoded: String): Boolean { - // cleaning up some bug from some client. - val cleanedUp = encoded.removeSuffix("-null") - - val l = cleanedUp.length - if (l < 28) return false - return cleanedUp[l - 28] == '?' && - cleanedUp[l - 27] == 'i' && - cleanedUp[l - 26] == 'v' && - cleanedUp[l - 25] == '=' - } - - fun decodeFromNIP04(payload: String): EncryptedInfo? = - try { - // cleaning up some bug from some client. - val parts = payload.removeSuffix("-null").split("?iv=") - EncryptedInfo( - ciphertext = Base64.getDecoder().decode(parts[0]), - nonce = Base64.getDecoder().decode(parts[1]), - ) - } catch (e: Exception) { - Log.w("NIP04", "Unable to Parse encrypted payload: $payload") - null - } - } - - fun encodePayload(): String = - Base64 - .getEncoder() - .encodeToString( - byteArrayOf(V.toByte()) + nonce + ciphertext, - ) - - fun encodeToNIP04(): String { - val nonce = Base64.getEncoder().encodeToString(nonce) - val ciphertext = Base64.getEncoder().encodeToString(ciphertext) - return "$ciphertext?iv=$nonce" - } + fun isNIP04(encoded: String) = Nip04Encoder.isNIP04(encoded) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/PrivateDmEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/PrivateDmEvent.kt index 84bb7302b..6f73e3195 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/PrivateDmEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/PrivateDmEvent.kt @@ -24,15 +24,16 @@ import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.experimental.inlineMetadata.Nip54InlineMetadata import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.core.any import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohashMipMap -import com.vitorpamplona.quartz.nip17Dm.ChatroomKey -import com.vitorpamplona.quartz.nip17Dm.ChatroomKeyable -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer -import com.vitorpamplona.quartz.nip36SensitiveContent.ContentWarningSerializer -import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetup -import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetupSerializer -import com.vitorpamplona.quartz.nip57Zaps.zapraiser.ZapRaiserSerializer +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.nip01Core.tags.people.pTag +import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag +import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey +import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable +import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.nip92IMeta.IMetaTag import com.vitorpamplona.quartz.utils.Hex import com.vitorpamplona.quartz.utils.TimeUtils @@ -63,7 +64,7 @@ class PrivateDmEvent( * nip-04 EncryptedDmEvent but may omit the recipient, too. This value can be queried and used for * initial messages. */ - private fun recipientPubKey() = tags.firstOrNull { it.size > 1 && it[0] == "p" }?.get(1) + private fun recipientPubKey() = tags.firstNotNullOfOrNull(PTag::parseKey) fun recipientPubKeyBytes() = recipientPubKey()?.runCatching { Hex.decode(this) }?.getOrNull() @@ -86,9 +87,9 @@ class PrivateDmEvent( * Nip-18 messages should refer to other events by inline references in the content like * `[](e/c06f795e1234a9a1aecc731d768d4f3ca73e80031734767067c82d67ce82e506). */ - fun replyTo() = tags.firstOrNull { it.size > 1 && it[0] == "e" }?.get(1) + fun replyTo() = tags.firstNotNullOfOrNull(MarkedETag::parseId) - fun with(pubkeyHex: String): Boolean = pubkeyHex == pubKey || tags.any { it.size > 1 && it[0] == "p" && it[1] == pubkeyHex } + fun with(pubkeyHex: HexKey): Boolean = pubkeyHex == pubKey || tags.any(PTag::isTagged, pubkeyHex) fun cachedContentFor(signer: NostrSigner): String? = decryptedContent[signer.pubKey] @@ -120,62 +121,33 @@ class PrivateDmEvent( const val ALT = "Private Message" const val NIP_18_ADVERTISEMENT = "[//]: # (nip18)\n" - fun create( - recipientPubKey: HexKey, + fun prepareMessageToEncrypt( msg: String, - replyTos: List? = null, - mentions: List? = null, - zapReceiver: List? = null, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - publishedRecipientPubKey: HexKey? = null, - advertiseNip18: Boolean = true, - markAsSensitive: Boolean, - zapRaiserAmount: Long?, - geohash: String? = null, imetas: List? = null, - isDraft: Boolean, - onReady: (PrivateDmEvent) -> Unit, - ) { + advertiseNip18: Boolean = true, + ): String { var message = msg imetas?.forEach { message = message.replace(it.url, Nip54InlineMetadata().createUrl(it.url, it.properties)) } - message = - if (advertiseNip18) { - NIP_18_ADVERTISEMENT + message - } else { - message - } - - val tags = mutableListOf>() - publishedRecipientPubKey?.let { tags.add(arrayOf("p", publishedRecipientPubKey)) } - replyTos?.forEach { tags.add(arrayOf("e", it, "", "reply")) } - mentions?.forEach { tags.add(arrayOf("p", it)) } - zapReceiver?.forEach { tags.add(ZapSplitSetupSerializer.toTagArray(it)) } - zapRaiserAmount?.let { tags.add(ZapRaiserSerializer.toTagArray(it)) } - - if (markAsSensitive) { - tags.add(ContentWarningSerializer.toTagArray()) + return if (advertiseNip18) { + NIP_18_ADVERTISEMENT + message + } else { + message } + } - geohash?.let { tags.addAll(geohashMipMap(it)) } - /* Privacy issue: DO NOT ADD THESE TO THE TAGS. - imetas?.forEach { - tags.add(Nip92MediaAttachments.createTag(it)) - } - */ + fun build( + to: PTag, + encryptedMessage: String, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, encryptedMessage, createdAt) { + alt(ALT) + pTag(to) - tags.add(AltTagSerializer.toTagArray(ALT)) - - signer.nip04Encrypt(message, recipientPubKey) { content -> - if (isDraft) { - signer.assembleRumor(createdAt, KIND, tags.toTypedArray(), content, onReady) - } else { - signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady) - } - } + initializer() } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/TagArrayBuilderExt.kt new file mode 100644 index 000000000..c5a018155 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/TagArrayBuilderExt.kt @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip04Dm + +import com.vitorpamplona.quartz.nip01Core.EventHintBundle +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.tags.events.EventReference +import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag + +fun TagArrayBuilder.reply(tag: EventReference) = add(MarkedETag.assemble(tag.eventId, tag.relayHint, MarkedETag.MARKER.REPLY, tag.author)) + +fun TagArrayBuilder.reply(tag: EventHintBundle) = add(tag.toMarkedETag(MarkedETag.MARKER.REPLY).toTagArray()) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip09Deletions/DeletionEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip09Deletions/DeletionEvent.kt index 2bf285381..7eeb1be6d 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip09Deletions/DeletionEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip09Deletions/DeletionEvent.kt @@ -24,11 +24,20 @@ import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip01Core.tags.addressables.aTag import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedAddresses +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag +import com.vitorpamplona.quartz.nip01Core.tags.events.eTag import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEventIds import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEvents -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer +import com.vitorpamplona.quartz.nip01Core.tags.kinds.kinds +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.nip01Core.tags.people.pTag +import com.vitorpamplona.quartz.nip01Core.tags.people.pTagIds +import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.utils.TimeUtils @Immutable @@ -46,59 +55,47 @@ class DeletionEvent( fun deleteAddresses() = taggedAddresses() - fun deleteAddressTags() = tags.mapNotNull { if (it.size > 1 && it[0] == "a") it[1] else null } + fun deleteAddressTags() = tags.mapNotNull(ATag::parseAddress) companion object { const val KIND = 5 const val ALT = "Deletion event" - fun create( + fun build( deleteEvents: List, - signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (DeletionEvent) -> Unit, - ) { - val content = "" - val tags = mutableListOf>() + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, "", createdAt) { + alt(ALT) - val kinds = - deleteEvents - .mapTo(HashSet()) { - "${it.kind}" - }.map { - arrayOf("k", it) - } + deleteEvents.forEach { + eTag(ETag(it.id)) + if (it is AddressableEvent) { + aTag(it.aTag()) + } + } - tags.addAll(deleteEvents.map { arrayOf("e", it.id) }) - tags.addAll(deleteEvents.mapNotNull { if (it is AddressableEvent) arrayOf("a", it.address().toTag()) else null }) - tags.addAll(kinds) - tags.add(AltTagSerializer.toTagArray(ALT)) + pTagIds(deleteEvents.mapTo(HashSet()) { it.pubKey }) + kinds(deleteEvents.mapTo(HashSet()) { it.kind }) - signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady) + initializer() } - fun createForVersionOnly( + fun buildForVersionOnly( deleteEvents: List, - signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (DeletionEvent) -> Unit, - ) { - val content = "" - val tags = mutableListOf>() + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, "", createdAt) { + alt(ALT) - val kinds = - deleteEvents - .mapTo(HashSet()) { - "${it.kind}" - }.map { - arrayOf("k", it) - } + deleteEvents.forEach { + pTag(PTag(it.pubKey)) + eTag(ETag(it.id)) + } - tags.addAll(deleteEvents.map { arrayOf("e", it.id) }) - tags.addAll(kinds) - tags.add(AltTagSerializer.toTagArray(ALT)) + kinds(deleteEvents.mapTo(HashSet()) { it.kind }) - signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady) + initializer() } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/BaseTextNoteEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/BaseThreadedEvent.kt similarity index 74% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/BaseTextNoteEvent.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/BaseThreadedEvent.kt index 65a3b0bbe..310d67085 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/BaseTextNoteEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/BaseThreadedEvent.kt @@ -23,25 +23,24 @@ package com.vitorpamplona.quartz.nip10Notes import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedAddresses import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUsers import com.vitorpamplona.quartz.nip10Notes.content.findIndexTagsWithEventsOrAddresses import com.vitorpamplona.quartz.nip10Notes.content.findIndexTagsWithPeople -import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser +import com.vitorpamplona.quartz.nip10Notes.content.findNostrUris +import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress import com.vitorpamplona.quartz.nip19Bech32.entities.NEmbed import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile import com.vitorpamplona.quartz.nip19Bech32.entities.NPub import com.vitorpamplona.quartz.nip19Bech32.entities.Note -import com.vitorpamplona.quartz.nip19Bech32.parse -import com.vitorpamplona.quartz.nip19Bech32.parseAtag import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent -import com.vitorpamplona.quartz.nip72ModCommunities.CommunityDefinitionEvent +import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent +import com.vitorpamplona.quartz.utils.lastNotNullOfOrNull @Immutable -open class BaseTextNoteEvent( +open class BaseThreadedEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -52,43 +51,38 @@ open class BaseTextNoteEvent( ) : Event(id, pubKey, createdAt, kind, tags, content, sig) { fun mentions() = taggedUsers() - fun isAFork() = tags.any { it.size > 3 && (it[0] == "a" || it[0] == "e") && it[3] == "fork" } + fun markedRoot() = tags.firstNotNullOfOrNull(MarkedETag::parseRoot) - fun forkFromAddress() = - tags.firstOrNull { it.size > 3 && it[0] == "a" && it[3] == "fork" }?.let { - val aTagValue = it[1] - val relay = it.getOrNull(2) + fun unmarkedRoot() = tags.firstNotNullOfOrNull(MarkedETag::parseUnmarkedRoot) - ATag.parse(aTagValue, relay) - } + fun root() = markedRoot() ?: unmarkedRoot() - fun forkFromVersion() = tags.firstOrNull { it.size > 3 && it[0] == "e" && it[3] == "fork" }?.get(1) + fun markedReply() = tags.lastNotNullOfOrNull(MarkedETag::parseReply) - fun isForkFromAddressWithPubkey(authorHex: HexKey) = tags.any { it.size > 3 && it[0] == "a" && it[3] == "fork" && it[1].contains(authorHex) } + fun unmarkedReply() = tags.lastNotNullOfOrNull(MarkedETag::parseUnmarkedReply) - open fun markedReplyTos(): List { - val newStyleReply = tags.lastOrNull { it.size > 3 && it[0] == "e" && it[3] == "reply" }?.get(1) - val newStyleRoot = tags.lastOrNull { it.size > 3 && it[0] == "e" && it[3] == "root" }?.get(1) - return listOfNotNull(newStyleReply, newStyleRoot) - } + fun reply() = markedReply() ?: unmarkedReply() - open fun unMarkedReplyTos(): List = tags.filter { it.size > 1 && it.size <= 3 && it[0] == "e" }.map { it[1] } + fun threadTags() = tags.mapNotNull(MarkedETag::parseAllThreadTags) - open fun replyingTo(): HexKey? { - val oldStylePositional = tags.lastOrNull { it.size > 1 && it.size <= 3 && it[0] == "e" }?.get(1) - val newStyleReply = tags.lastOrNull { it.size > 3 && it[0] == "e" && it[3] == "reply" }?.get(1) - val newStyleRoot = tags.lastOrNull { it.size > 3 && it[0] == "e" && it[3] == "root" }?.get(1) + open fun markedReplyTos() = listOfNotNull(markedRoot()?.eventId, markedReply()?.eventId) - return newStyleReply ?: newStyleRoot ?: oldStylePositional - } + open fun unmarkedReplyTos(): List = tags.mapNotNull(MarkedETag::parseOnlyPositionalThreadTagsIds) + open fun replyingTo() = + markedReply()?.eventId + ?: markedRoot()?.eventId + ?: unmarkedReply()?.eventId + + /* + Not sure if this is needed open fun replyingToAddress(): ATag? { val oldStylePositional = tags.lastOrNull { it.size > 1 && it.size <= 3 && it[0] == "a" }?.let { ATag.parseAtag(it[1], it[2]) } val newStyleReply = tags.lastOrNull { it.size > 3 && it[0] == "a" && it[3] == "reply" }?.let { ATag.parseAtag(it[1], it[2]) } val newStyleRoot = tags.lastOrNull { it.size > 3 && it[0] == "a" && it[3] == "root" }?.let { ATag.parseAtag(it[1], it[2]) } return newStyleReply ?: newStyleRoot ?: oldStylePositional - } + }*/ open fun replyingToAddressOrEvent(): String? { val oldStylePositional = tags.lastOrNull { it.size > 1 && it.size <= 3 && (it[0] == "e" || it[0] == "a") }?.get(1) @@ -110,8 +104,7 @@ open class BaseTextNoteEvent( val citedUsers = mutableSetOf() findIndexTagsWithPeople(content, tags, citedUsers) - - Nip19Parser.parseAll(content).forEach { parsed -> + findNostrUris(content).forEach { parsed -> when (parsed) { is NProfile -> citedUsers.add(parsed.hex) is NPub -> citedUsers.add(parsed.hex) @@ -130,8 +123,7 @@ open class BaseTextNoteEvent( val citations = mutableSetOf() findIndexTagsWithEventsOrAddresses(content, tags, citations).toMutableSet() - - Nip19Parser.parseAll(content).forEach { entity -> + findNostrUris(content).forEach { entity -> when (entity) { is NEvent -> citations.add(entity.hex) is NAddress -> citations.add(entity.aTag()) @@ -146,7 +138,7 @@ open class BaseTextNoteEvent( fun tagsWithoutCitations(): List { val certainRepliesTo = markedReplyTos() - val uncertainRepliesTo = unMarkedReplyTos() + val uncertainRepliesTo = unmarkedReplyTos() val tagAddresses = taggedAddresses() diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/TextNoteEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/TextNoteEvent.kt index 8987c7909..00bfe4130 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/TextNoteEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/TextNoteEvent.kt @@ -21,24 +21,13 @@ package com.vitorpamplona.quartz.nip10Notes import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.EventHintBundle import com.vitorpamplona.quartz.nip01Core.HexKey -import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent -import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag -import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohashMipMap -import com.vitorpamplona.quartz.nip01Core.tags.hashtags.buildHashtagTags -import com.vitorpamplona.quartz.nip10Notes.content.buildUrlRefs -import com.vitorpamplona.quartz.nip10Notes.content.findHashtags -import com.vitorpamplona.quartz.nip10Notes.content.findURLs -import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrl -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer -import com.vitorpamplona.quartz.nip36SensitiveContent.ContentWarningSerializer -import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetup -import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetupSerializer -import com.vitorpamplona.quartz.nip57Zaps.zapraiser.ZapRaiserSerializer -import com.vitorpamplona.quartz.nip92IMeta.IMetaTag -import com.vitorpamplona.quartz.nip92IMeta.Nip92MediaAttachments +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip10Notes.tags.markedETags +import com.vitorpamplona.quartz.nip10Notes.tags.prepareETagsAsReplyTo +import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.utils.TimeUtils @Immutable @@ -49,9 +38,7 @@ class TextNoteEvent( tags: Array>, content: String, sig: HexKey, -) : BaseTextNoteEvent(id, pubKey, createdAt, KIND, tags, content, sig) { - fun root() = tags.firstOrNull { it.size > 3 && it[3] == "root" }?.get(1) - +) : BaseThreadedEvent(id, pubKey, createdAt, KIND, tags, content, sig) { companion object { const val KIND = 1 const val ALT = "A short note: " @@ -61,123 +48,20 @@ class TextNoteEvent( return ALT + msg.take(50) + "..." } - fun create( - msg: String, - replyTos: List? = null, - mentions: List? = null, - addresses: List? = null, - extraTags: List? = null, - zapReceiver: List? = null, - markAsSensitive: Boolean = false, - zapRaiserAmount: Long? = null, - replyingTo: String? = null, - root: String? = null, - directMentions: Set = emptySet(), - geohash: String? = null, - imetas: List? = null, - emojis: List? = null, - forkedFrom: Event? = null, - signer: NostrSigner, + fun build( + note: String, + replyingTo: EventHintBundle? = null, + forkingFrom: EventHintBundle? = null, createdAt: Long = TimeUtils.now(), - isDraft: Boolean, - onReady: (TextNoteEvent) -> Unit, - ) { - val tags = mutableListOf>() - tags.add(AltTagSerializer.toTagArray(shortedMessageForAlt(msg))) - replyTos?.let { - tags.addAll( - it.positionalMarkedTags( - tagName = "e", - root = root, - replyingTo = replyingTo, - directMentions = directMentions, - forkedFrom = forkedFrom?.id, - ), - ) - } - mentions?.forEach { - if (it in directMentions) { - tags.add(arrayOf("p", it, "", "mention")) - } else { - tags.add(arrayOf("p", it)) - } - } - replyTos?.forEach { - if (it in directMentions) { - tags.add(arrayOf("q", it)) - } - } - addresses?.forEach { - if (it.toTag() in directMentions) { - tags.add(arrayOf("q", it.toTag())) - } - } - addresses - ?.map { it.toTag() } - ?.let { - tags.addAll( - it.positionalMarkedTags( - tagName = "a", - root = root, - replyingTo = replyingTo, - directMentions = directMentions, - forkedFrom = (forkedFrom as? AddressableEvent)?.address()?.toTag(), - ), - ) - } - tags.addAll(buildHashtagTags(findHashtags(msg) + (extraTags ?: emptyList()))) - tags.addAll(buildUrlRefs(findURLs(msg))) - zapReceiver?.forEach { tags.add(ZapSplitSetupSerializer.toTagArray(it)) } - zapRaiserAmount?.let { tags.add(ZapRaiserSerializer.toTagArray(it)) } + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, note, createdAt) { + alt(shortedMessageForAlt(note)) - if (markAsSensitive) { - tags.add(ContentWarningSerializer.toTagArray()) + if (replyingTo != null || forkingFrom != null) { + markedETags(prepareETagsAsReplyTo(replyingTo, forkingFrom)) } - geohash?.let { tags.addAll(geohashMipMap(it)) } - imetas?.forEach { tags.add(Nip92MediaAttachments.createTag(it)) } - emojis?.forEach { tags.add(it.toTagArray()) } - - if (isDraft) { - signer.assembleRumor(createdAt, KIND, tags.toTypedArray(), msg, onReady) - } else { - signer.sign(createdAt, KIND, tags.toTypedArray(), msg, onReady) - } + initializer() } } } - -/** - * Returns a list of NIP-10 marked tags that are also ordered at best effort to support the - * deprecated method of positional tags to maximize backwards compatibility with clients that - * support replies but have not been updated to understand tag markers. - * - * https://github.com/nostr-protocol/nips/blob/master/10.md - * - * The tag to the root of the reply chain goes first. The tag to the reply event being responded - * to goes last. The order for any other tag does not matter, so keep the relative order. - */ -fun List.positionalMarkedTags( - tagName: String, - root: String?, - replyingTo: String?, - directMentions: Set, - forkedFrom: String?, -) = sortedWith { o1, o2 -> - when { - o1 == o2 -> 0 - o1 == root -> -1 // root goes first - o2 == root -> 1 // root goes first - o1 == replyingTo -> 1 // reply event being responded to goes last - o2 == replyingTo -> -1 // reply event being responded to goes last - else -> 0 // keep the relative order for any other tag - } -}.map { - when (it) { - root -> arrayOf(tagName, it, "", "root") - replyingTo -> arrayOf(tagName, it, "", "reply") - forkedFrom -> arrayOf(tagName, it, "", "fork") - in directMentions -> arrayOf(tagName, it, "", "mention") - else -> arrayOf(tagName, it) - } -} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/content/IndexedTags.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/content/IndexedTags.kt index 8c3f1fbb7..0af01dee0 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/content/IndexedTags.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/content/IndexedTags.kt @@ -23,6 +23,10 @@ package com.vitorpamplona.quartz.nip10Notes.content import com.vitorpamplona.quartz.nip01Core.core.TagArray import java.util.regex.Pattern +/** + * this is the old way of linking an e tag on the content by [index] the tag + */ + val tagSearch = Pattern.compile("(?:\\s|\\A)\\#\\[([0-9]+)\\]") /** diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/content/NostrUris.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/content/NostrUris.kt new file mode 100644 index 000000000..384561087 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/content/NostrUris.kt @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip10Notes.content + +import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser + +fun findNostrUris(content: String) = Nip19Parser.parseAll(content) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/tags/MarkedETag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/tags/MarkedETag.kt new file mode 100644 index 000000000..b4ddaa5e1 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/tags/MarkedETag.kt @@ -0,0 +1,251 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip10Notes.tags + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.tags.events.GenericETag +import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent +import com.vitorpamplona.quartz.utils.arrayOfNotNull +import com.vitorpamplona.quartz.utils.bytesUsedInMemory +import com.vitorpamplona.quartz.utils.pointerSizeInBytes + +@Immutable +data class MarkedETag( + override val eventId: HexKey, +) : GenericETag { + override var relay: String? = null + var marker: String? = null + override var author: HexKey? = null + + constructor(eventId: HexKey, relayHint: String? = null, marker: String? = null, authorPubKeyHex: HexKey? = null) : this(eventId) { + this.relay = relayHint + this.marker = marker + this.author = authorPubKeyHex + } + + constructor(eventId: HexKey, relayHint: String? = null, marker: MARKER? = null, authorPubKeyHex: HexKey? = null) : this(eventId) { + this.relay = relayHint + this.marker = marker?.code + this.author = authorPubKeyHex + } + + fun countMemory(): Long = + 4 * pointerSizeInBytes + // 3 fields, 4 bytes each reference (32bit) + eventId.bytesUsedInMemory() + + (relay?.bytesUsedInMemory() ?: 0) + + (marker?.bytesUsedInMemory() ?: 0) + + (author?.bytesUsedInMemory() ?: 0) + + fun toNEvent(): String = NEvent.create(eventId, author, null, relay) + + override fun toTagArray() = arrayOfNotNull(TAG_NAME, eventId, relay, marker, author) + + enum class MARKER( + val code: String, + ) { + ROOT("root"), + REPLY("reply"), + MENTION("mention"), + FORK("fork"), + } + + companion object { + const val TAG_NAME = "e" + const val TAG_SIZE = 4 + + const val ORDER_NAME = 0 + const val ORDER_EVT_ID = 1 + const val ORDER_RELAY = 2 + const val ORDER_MARKER = 3 + const val ORDER_PUBKEY = 4 + + fun isTagged( + tag: Array, + key: HexKey, + ) = tag.size >= 2 && tag[0] == TAG_NAME && tag[1] == key + + @JvmStatic + fun parse(tag: Array): MarkedETag? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + + return MarkedETag( + tag[ORDER_EVT_ID], + tag[ORDER_RELAY], + tag[ORDER_MARKER], + tag.getOrNull( + ORDER_PUBKEY, + ), + ) + } + + @JvmStatic + fun parseId(tag: Array): HexKey? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + + return tag[ORDER_EVT_ID] + } + + @JvmStatic + fun parseAllThreadTags(tag: Array): MarkedETag? = + if (tag.size >= 2 && tag[0] == TAG_NAME) { + if (tag.size <= 3) { + // simple case ["e", "id", "relay"] + MarkedETag(tag[1], tag.getOrNull(2), null as String?, null) + } else if (tag.size == 4) { + if (tag[3].isEmpty()) { + // empty tags ["e", "id", "relay", ""] + MarkedETag(tag[1], tag[2], null as String?, null) + } else if (tag[3].length == 64) { + // updated case with pubkey instead of marker ["e", "id", "relay", "pubkey"] + MarkedETag(tag[1], tag[2], null as String?, tag[3]) + } else if (tag[3] == MARKER.ROOT.code) { + // corrent root ["e", "id", "relay", "root"] + MarkedETag(tag[1], tag[2], tag[3]) + } else if (tag[3] == MARKER.REPLY.code) { + // correct reply ["e", "id", "relay", "reply"] + MarkedETag(tag[1], tag[2], tag[3]) + } else { + // ignore "mention" and "fork" markers + null + } + } else { + // tag.size >= 5 + if (tag[3].isEmpty()) { + // empty tags ["e", "id", "relay", "", "pubkey"] + MarkedETag(tag[1], tag[2], null as String?, tag[4]) + } else if (tag[3].length == 64) { + // updated case with pubkey instead of marker ["e", "id", "relay", "pubkey"] + MarkedETag(tag[1], tag[2], null as String?, tag[3]) + } else if (tag[3] == MARKER.ROOT.code) { + // corrent root ["e", "id", "relay", "root"] + MarkedETag(tag[1], tag[2], tag[3], tag[4]) + } else if (tag[3] == MARKER.REPLY.code) { + // correct reply ["e", "id", "relay", "reply"] + MarkedETag(tag[1], tag[2], tag[3], tag[4]) + } else { + // ignore "mention" and "fork" markers + null + } + } + } else { + null + } + + @JvmStatic + fun parseOnlyPositionalThreadTagsIds(tag: Array): HexKey? = + if (tag.size >= 2 && tag[0] == TAG_NAME) { + if (tag.size <= 3) { + // simple case ["e", "id"] + // simple case ["e", "id", "relay"] + tag[1] + } else if (tag.size == 4) { + if (tag[3].isEmpty() || tag[3].length == 64) { + // empty tags ["e", "id", "relay", ""] + tag[1] + } else { + // ignore all markers + null + } + } else { + // tag.size >= 5 + if (tag[3].isEmpty() || tag[3].length == 64) { + // empty tags ["e", "id", "relay", "", "pubkey"] + // updated case with pubkey instead of marker ["e", "id", "relay", "pubkey"] + tag[1] + } else { + // ignore all markers + null + } + } + } else { + null + } + + @JvmStatic + fun parseRoot(tag: Array): MarkedETag? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + if (tag[ORDER_MARKER] != MARKER.ROOT.code) return null + // ["e", id hex, relay hint, marker, pubkey] + return MarkedETag( + tag[ORDER_EVT_ID], + tag[ORDER_RELAY], + tag[ORDER_MARKER], + tag.getOrNull( + ORDER_PUBKEY, + ), + ) + } + + /** + * Old positional arguments + */ + @JvmStatic + fun parseUnmarkedRoot(tag: Array): MarkedETag? = + if (tag.size in 2..3 && tag[0] == TAG_NAME) { + MarkedETag(tag[1], tag.getOrNull(2), MARKER.ROOT) + } else { + null + } + + @JvmStatic + fun parseReply(tag: Array): MarkedETag? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + if (tag[ORDER_MARKER] != MARKER.REPLY.code) return null + // ["e", id hex, relay hint, marker, pubkey] + return MarkedETag( + tag[ORDER_EVT_ID], + tag[ORDER_RELAY], + tag[ORDER_MARKER], + tag.getOrNull( + ORDER_PUBKEY, + ), + ) + } + + /** + * Old positional arguments + */ + @JvmStatic + fun parseUnmarkedReply(tag: Array): MarkedETag? = + if (tag.size in 2..3 && tag[0] == TAG_NAME) { + MarkedETag(tag[1], tag.getOrNull(2), MARKER.REPLY) + } else { + null + } + + @JvmStatic + fun parseRootId(tag: Array): HexKey? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + if (tag[ORDER_MARKER] != MARKER.ROOT.code) return null + // ["e", id hex, relay hint, marker, pubkey] + return tag[ORDER_EVT_ID] + } + + @JvmStatic + fun assemble( + eventId: HexKey, + relay: String?, + marker: MARKER?, + author: HexKey?, + ) = arrayOfNotNull(TAG_NAME, eventId, relay, marker?.code, author) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/tags/Positional.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/tags/Positional.kt new file mode 100644 index 000000000..7b0947b38 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/tags/Positional.kt @@ -0,0 +1,78 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip10Notes.tags + +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag + +/** + * Returns a list of NIP-10 marked tags that are also ordered at best effort to support the + * deprecated method of positional tags to maximize backwards compatibility with clients that + * support replies but have not been updated to understand tag markers. + * + * https://github.com/nostr-protocol/nips/blob/master/10.md + * + * The tag to the root of the reply chain goes first. The tag to the reply event being responded + * to goes last. The order for any other tag does not matter, so keep the relative order. + */ +fun List.positionalMarkedTags( + tagName: String, + root: String?, + replyingTo: String?, + forkedFrom: String?, +) = sortedWith { o1, o2 -> + when { + o1 == o2 -> 0 + o1 == root -> -1 // root goes first + o2 == root -> 1 // root goes first + o1 == replyingTo -> 1 // reply event being responded to goes last + o2 == replyingTo -> -1 // reply event being responded to goes last + else -> 0 // keep the relative order for any other tag + } +}.map { + when (it) { + root -> arrayOf(tagName, it, "", "root") + replyingTo -> arrayOf(tagName, it, "", "reply") + forkedFrom -> arrayOf(tagName, it, "", "fork") + else -> arrayOf(tagName, it) + } +} + +fun List.positionalMarkedTags( + root: ETag?, + replyingTo: ETag?, + forkedFrom: ETag?, +) = sortedWith { o1, o2 -> + when { + o1.eventId == o2.eventId -> 0 + o1.eventId == root?.eventId -> -1 // root goes first + o2.eventId == root?.eventId -> 1 // root goes first + o1.eventId == replyingTo?.eventId -> 1 // reply event being responded to goes last + o2.eventId == replyingTo?.eventId -> -1 // reply event being responded to goes last + else -> 0 // keep the relative order for any other tag + } +}.map { + when (it.eventId) { + root?.eventId -> MarkedETag(it.eventId, it.relay, MarkedETag.MARKER.ROOT, it.author) + replyingTo?.eventId -> MarkedETag(it.eventId, it.relay, MarkedETag.MARKER.REPLY, it.author) + forkedFrom?.eventId -> MarkedETag(it.eventId, it.relay, MarkedETag.MARKER.FORK, it.author) + else -> it + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/tags/ReplyBuilder.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/tags/ReplyBuilder.kt new file mode 100644 index 000000000..95f653af6 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/tags/ReplyBuilder.kt @@ -0,0 +1,94 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip10Notes.tags + +import com.vitorpamplona.quartz.nip01Core.EventHintBundle +import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent +import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent + +fun prepareETagsAsReplyTo( + replyingTo: EventHintBundle? = null, + forkingFrom: EventHintBundle? = null, +): List { + if (replyingTo == null && forkingFrom == null) return emptyList() + + val forkTag = forkingFrom?.toMarkedETag(MarkedETag.MARKER.FORK) + + if (replyingTo == null) { + return listOfNotNull(forkTag) + } + + val rootTag = replyingTo.event.markedRoot() ?: replyingTo.event.unmarkedRoot() + + if (rootTag == null) { + return listOfNotNull(replyingTo.toMarkedETag(MarkedETag.MARKER.ROOT), forkTag) + } + + val replyTag = replyingTo.event.markedReply() ?: replyingTo.event.unmarkedReply() + + if (replyTag == null || replyTag.eventId == rootTag.eventId) { + return listOfNotNull( + rootTag, + forkTag, + replyingTo.toMarkedETag(MarkedETag.MARKER.REPLY), + ) + } + + val branchTags = + replyingTo.event.threadTags().filter { it.eventId != rootTag.eventId }.map { + MarkedETag(it.eventId, it.relay, "", it.author) + } + + val branch = mutableListOf() + branch.add(rootTag) + branch.addAll(branchTags) + forkTag?.let { branch.add(forkTag) } + branch.add(replyingTo.toMarkedETag(MarkedETag.MARKER.REPLY)) + return branch +} + +fun prepareMarkedETagsAsReplyTo(replyingTo: EventHintBundle): List { + val rootTag = replyingTo.event.markedRoot() ?: replyingTo.event.unmarkedRoot() + + if (rootTag == null) { + return listOfNotNull(replyingTo.toMarkedETag(MarkedETag.MARKER.ROOT)) + } + + val replyTag = replyingTo.event.markedReply() ?: replyingTo.event.unmarkedReply() + + if (replyTag == null || replyTag.eventId == rootTag.eventId) { + return listOfNotNull( + rootTag, + replyingTo.toMarkedETag(MarkedETag.MARKER.REPLY), + ) + } + + val branchTags = + replyingTo.event.threadTags().filter { it.eventId != rootTag.eventId }.map { + MarkedETag(it.eventId, it.relay, "", it.author) + } + + val branch = mutableListOf() + branch.add(rootTag) + branch.addAll(branchTags) + branch.add(replyingTo.toMarkedETag(MarkedETag.MARKER.REPLY)) + return branch +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/tags/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/tags/TagArrayBuilderExt.kt new file mode 100644 index 000000000..75d0088f2 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/tags/TagArrayBuilderExt.kt @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip10Notes.tags + +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent +import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent + +fun TagArrayBuilder.markedETag(tag: MarkedETag) = add(tag.toTagArray()) + +fun TagArrayBuilder.markedETags(tag: List) = addAll(tag.map { it.toTagArray() }) + +fun TagArrayBuilder.notify(tag: PTag) = add(tag.toTagArray()) + +fun TagArrayBuilder.notify(tag: List) = addAll(tag.map { it.toTagArray() }) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/PoWTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/PoWTag.kt index c56efad21..fd372c122 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/PoWTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/PoWTag.kt @@ -26,7 +26,7 @@ import com.vitorpamplona.quartz.utils.pointerSizeInBytes class PoWTag( val nonce: String, - val commitment: String?, + val commitment: Int?, ) { fun countMemory(): Long = 2 * pointerSizeInBytes + nonce.bytesUsedInMemory() + (commitment?.bytesUsedInMemory() ?: 0) @@ -34,17 +34,27 @@ class PoWTag( companion object { const val TAG_NAME = "nonce" + const val TAG_SIZE = 2 @JvmStatic - fun parse(tags: Array): PoWTag { - require(tags[0] == TAG_NAME) - return PoWTag(tags[1], tags.getOrNull(2)) + fun hasTagWithContent(tag: Array) = tag.size >= TAG_SIZE && tag[0] == TAG_NAME && tag[1].isNotEmpty() + + @JvmStatic + fun parse(tag: Array): PoWTag? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return PoWTag(tag[1], tag.getOrNull(2)?.toIntOrNull()) + } + + @JvmStatic + fun parseCommitment(tag: Array): Int? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag.getOrNull(2)?.toIntOrNull() } @JvmStatic fun assemble( nonce: String, - commitment: String?, - ) = arrayOfNotNull(TAG_NAME, nonce, commitment) + commitment: Int?, + ) = arrayOfNotNull(TAG_NAME, nonce, commitment.toString()) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/TagArrayBuilderExt.kt index 7213dd466..5713dd502 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/TagArrayBuilderExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/TagArrayBuilderExt.kt @@ -20,16 +20,12 @@ */ package com.vitorpamplona.quartz.nip13Pow +import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder -fun TagArrayBuilder.pow( +fun TagArrayBuilder.pow( nonce: String, commitment: Int, -) = add(PoWTag.assemble(nonce, commitment.toString())) - -fun TagArrayBuilder.pow( - nonce: String, - commitment: String, ) = add(PoWTag.assemble(nonce, commitment)) -fun TagArrayBuilder.pow(tag: PoWTag) = add(tag.toTagArray()) +fun TagArrayBuilder.pow(tag: PoWTag) = add(tag.toTagArray()) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/TagArrayExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/TagArrayExt.kt index a89aa2998..e0ffb0825 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/TagArrayExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/TagArrayExt.kt @@ -21,11 +21,9 @@ package com.vitorpamplona.quartz.nip13Pow import com.vitorpamplona.quartz.nip01Core.core.TagArray -import com.vitorpamplona.quartz.nip01Core.core.hasTagWithContent -import com.vitorpamplona.quartz.nip01Core.core.mapTagged -fun TagArray.commitedPoW() = this.firstOrNull { it.size > 2 && it[0] == "nonce" }?.get(2)?.toIntOrNull() +fun TagArray.commitedPoW() = this.firstNotNullOfOrNull(PoWTag::parseCommitment) -fun TagArray.hasPoW() = this.hasTagWithContent(PoWTag.TAG_NAME) +fun TagArray.hasPoW() = this.any(PoWTag::hasTagWithContent) -fun TagArray.powTags() = this.mapTagged(PoWTag.TAG_NAME) { PoWTag.parse(it) } +fun TagArray.powTags() = this.mapNotNull(PoWTag::parse) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip14Subject/SubjectTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip14Subject/SubjectTag.kt index 5baf21f0a..8489b1669 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip14Subject/SubjectTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip14Subject/SubjectTag.kt @@ -20,24 +20,18 @@ */ package com.vitorpamplona.quartz.nip14Subject -import com.vitorpamplona.quartz.nip01Core.tags.dTags.DTag -import com.vitorpamplona.quartz.utils.bytesUsedInMemory -import com.vitorpamplona.quartz.utils.pointerSizeInBytes - -class SubjectTag( - val subject: String, -) { - fun countMemory(): Long = 1 * pointerSizeInBytes + subject.bytesUsedInMemory() - - fun toTagArray() = assemble(subject) - +class SubjectTag { companion object { const val TAG_NAME = "subject" + const val TAG_SIZE = 2 @JvmStatic - fun parse(tags: Array): DTag { - require(tags[0] == TAG_NAME) - return DTag(tags[1]) + fun hasTagWithContent(tag: Array) = tag.size >= TAG_SIZE && tag[0] == TAG_NAME && tag[1].isNotEmpty() + + @JvmStatic + fun parse(tag: Array): String? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1] } @JvmStatic diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip14Subject/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip14Subject/TagArrayBuilderExt.kt index bae13bc22..816eebc08 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip14Subject/TagArrayBuilderExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip14Subject/TagArrayBuilderExt.kt @@ -21,5 +21,6 @@ package com.vitorpamplona.quartz.nip14Subject import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent -fun TagArrayBuilder.subject(subject: String) = add(SubjectTag.assemble(subject)) +fun TagArrayBuilder.subject(subject: String) = addUnique(SubjectTag.assemble(subject)) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip14Subject/TagArrayExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip14Subject/TagArrayExt.kt index 4e7f538c8..0300d80fe 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip14Subject/TagArrayExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip14Subject/TagArrayExt.kt @@ -21,12 +21,9 @@ package com.vitorpamplona.quartz.nip14Subject import com.vitorpamplona.quartz.nip01Core.core.TagArray -import com.vitorpamplona.quartz.nip01Core.core.firstTagValue -import com.vitorpamplona.quartz.nip01Core.core.hasTagWithContent -import com.vitorpamplona.quartz.nip01Core.core.mapValues -fun TagArray.subject() = this.firstTagValue(SubjectTag.TAG_NAME) +fun TagArray.subject() = this.firstNotNullOfOrNull(SubjectTag::parse) -fun TagArray.subjects() = this.mapValues(SubjectTag.TAG_NAME) +fun TagArray.subjects() = this.mapNotNull(SubjectTag::parse) -fun TagArray.hasSubject() = this.hasTagWithContent(SubjectTag.TAG_NAME) +fun TagArray.hasSubject() = this.any(SubjectTag::hasTagWithContent) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/ChatMessageEncryptedFileHeaderEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/ChatMessageEncryptedFileHeaderEvent.kt deleted file mode 100644 index 003a403b3..000000000 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/ChatMessageEncryptedFileHeaderEvent.kt +++ /dev/null @@ -1,191 +0,0 @@ -/** - * Copyright (c) 2024 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.quartz.nip17Dm - -import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey -import com.vitorpamplona.quartz.nip01Core.hexToByteArray -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip01Core.toHexKey -import com.vitorpamplona.quartz.nip36SensitiveContent.ContentWarningSerializer -import com.vitorpamplona.quartz.nip59Giftwrap.WrappedEvent -import com.vitorpamplona.quartz.nip94FileMetadata.Dimension -import com.vitorpamplona.quartz.utils.TimeUtils -import kotlinx.collections.immutable.toImmutableSet - -@Immutable -class ChatMessageEncryptedFileHeaderEvent( - id: HexKey, - pubKey: HexKey, - createdAt: Long, - tags: Array>, - content: String, - sig: HexKey, -) : WrappedEvent(id, pubKey, createdAt, KIND, tags, content, sig), - ChatroomKeyable, - NIP17Group { - /** Recipients intended to receive this conversation */ - fun recipientsPubKey() = tags.mapNotNull { if (it.size > 1 && it[0] == "p") it[1] else null } - - override fun groupMembers() = recipientsPubKey().plus(pubKey).toSet() - - fun replyTo() = tags.firstOrNull { it.size > 1 && it[0] == "e" }?.get(1) - - fun talkingWith(oneSideHex: String): Set { - val listedPubKeys = recipientsPubKey() - - val result = - if (pubKey == oneSideHex) { - listedPubKeys.toSet().minus(oneSideHex) - } else { - listedPubKeys.plus(pubKey).toSet().minus(oneSideHex) - } - - if (result.isEmpty()) { - // talking to myself - return setOf(pubKey) - } - - return result - } - - override fun chatroomKey(toRemove: String): ChatroomKey = ChatroomKey(talkingWith(toRemove).toImmutableSet()) - - fun url() = content - - fun mimeType() = tags.firstOrNull { it.size > 1 && it[0] == MIME_TYPE }?.get(1) - - fun alt() = tags.firstOrNull { it.size > 1 && it[0] == ALT }?.get(1) - - fun algo() = tags.firstOrNull { it.size > 1 && it[0] == ENCRYPTION_ALGORITHM }?.get(1) - - fun key() = - tags - .firstOrNull { it.size > 1 && it[0] == ENCRYPTION_KEY } - ?.get(1) - ?.runCatching { this.hexToByteArray() } - ?.getOrNull() - - fun nonce() = - tags - .firstOrNull { it.size > 1 && it[0] == ENCRYPTION_NONCE } - ?.get(1) - ?.runCatching { this.hexToByteArray() } - ?.getOrNull() - - fun hash() = tags.firstOrNull { it.size > 1 && it[0] == HASH }?.get(1) - - fun originalHash() = tags.firstOrNull { it.size > 1 && it[0] == ORIGINAL_HASH }?.get(1) - - fun size() = tags.firstOrNull { it.size > 1 && it[0] == FILE_SIZE }?.get(1) - - fun dimensions() = tags.firstOrNull { it.size > 1 && it[0] == DIMENSION }?.get(1)?.let { Dimension.parse(it) } - - fun blurhash() = tags.firstOrNull { it.size > 1 && it[0] == BLUR_HASH }?.get(1) - - companion object { - const val KIND = 15 - const val ALT_DESCRIPTION = "Encrypted file in chat" - - const val MIME_TYPE = "file-type" - - const val ENCRYPTION_ALGORITHM = "encryption-algorithm" - const val ENCRYPTION_KEY = "decryption-key" - const val ENCRYPTION_NONCE = "decryption-nonce" - - const val FILE_SIZE = "size" - const val DIMENSION = "dim" - const val BLUR_HASH = "blurhash" - const val HASH = "x" - const val ORIGINAL_HASH = "ox" - - const val ALT = "alt" - - fun buildTags( - to: List, - repliesTo: List? = null, - contentType: String?, - algo: String, - key: ByteArray, - nonce: ByteArray? = null, - originalHash: String? = null, - hash: String? = null, - size: Int? = null, - dimensions: Dimension? = null, - blurhash: String? = null, - sensitiveContent: Boolean? = null, - alt: String?, - ): Array> { - val repliesHex = repliesTo?.map { arrayOf("e", it) } ?: emptyList() - - return ( - to.map { arrayOf("p", it) } + repliesHex + - listOfNotNull( - contentType?.let { arrayOf(MIME_TYPE, it) }, - arrayOf(ENCRYPTION_ALGORITHM, algo), - arrayOf(ENCRYPTION_KEY, key.toHexKey()), - nonce?.let { arrayOf(ENCRYPTION_NONCE, it.toHexKey()) }, - alt?.ifBlank { null }?.let { arrayOf(ALT, it) } ?: arrayOf(ALT, ALT_DESCRIPTION), - originalHash?.let { arrayOf(ORIGINAL_HASH, it) }, - hash?.let { arrayOf(HASH, it) }, - size?.let { arrayOf(FILE_SIZE, it.toString()) }, - dimensions?.let { arrayOf(DIMENSION, it.toString()) }, - blurhash?.let { arrayOf(BLUR_HASH, it) }, - sensitiveContent?.let { - if (it) { - ContentWarningSerializer.toTagArray() - } else { - null - } - }, - ) - ).toTypedArray() - } - - fun create( - url: String, - to: List, - repliesTo: List? = null, - contentType: String?, - algo: String, - key: ByteArray, - nonce: ByteArray? = null, - originalHash: String? = null, - hash: String? = null, - size: Int? = null, - dimensions: Dimension? = null, - blurhash: String? = null, - sensitiveContent: Boolean? = null, - alt: String?, - signer: NostrSigner, - isDraft: Boolean, - createdAt: Long = TimeUtils.now(), - onReady: (ChatMessageEncryptedFileHeaderEvent) -> Unit, - ) { - val tags = buildTags(to, repliesTo, contentType, algo, key, nonce, originalHash, hash, size, dimensions, blurhash, sensitiveContent, alt) - if (isDraft) { - signer.assembleRumor(createdAt, KIND, tags, url, onReady) - } else { - signer.sign(createdAt, KIND, tags, url, onReady) - } - } - } -} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/ChatMessageEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/ChatMessageEvent.kt deleted file mode 100644 index dceb8aede..000000000 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/ChatMessageEvent.kt +++ /dev/null @@ -1,124 +0,0 @@ -/** - * Copyright (c) 2024 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.quartz.nip17Dm - -import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey -import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohashMipMap -import com.vitorpamplona.quartz.nip14Subject.subject -import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrl -import com.vitorpamplona.quartz.nip36SensitiveContent.ContentWarningSerializer -import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetup -import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetupSerializer -import com.vitorpamplona.quartz.nip57Zaps.zapraiser.ZapRaiserSerializer -import com.vitorpamplona.quartz.nip59Giftwrap.WrappedEvent -import com.vitorpamplona.quartz.nip92IMeta.IMetaTag -import com.vitorpamplona.quartz.nip92IMeta.Nip92MediaAttachments -import com.vitorpamplona.quartz.utils.TimeUtils -import kotlinx.collections.immutable.toImmutableSet - -@Immutable -class ChatMessageEvent( - id: HexKey, - pubKey: HexKey, - createdAt: Long, - tags: Array>, - content: String, - sig: HexKey, -) : WrappedEvent(id, pubKey, createdAt, KIND, tags, content, sig), - ChatroomKeyable, - NIP17Group { - /** Recipients intended to receive this conversation */ - fun recipientsPubKey() = tags.mapNotNull { if (it.size > 1 && it[0] == "p") it[1] else null } - - fun replyTo() = tags.firstOrNull { it.size > 1 && it[0] == "e" }?.get(1) - - fun talkingWith(oneSideHex: String): Set { - val listedPubKeys = recipientsPubKey() - - val result = - if (pubKey == oneSideHex) { - listedPubKeys.toSet().minus(oneSideHex) - } else { - listedPubKeys.plus(pubKey).toSet().minus(oneSideHex) - } - - if (result.isEmpty()) { - // talking to myself - return setOf(pubKey) - } - - return result - } - - override fun groupMembers() = recipientsPubKey().plus(pubKey).toSet() - - override fun chatroomKey(toRemove: String): ChatroomKey = ChatroomKey(talkingWith(toRemove).toImmutableSet()) - - companion object { - const val KIND = 14 - const val ALT = "Direct message" - - fun create( - msg: String, - to: List? = null, - subject: String? = null, - replyTos: List? = null, - mentions: List? = null, - zapReceiver: List? = null, - markAsSensitive: Boolean = false, - zapRaiserAmount: Long? = null, - geohash: String? = null, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - imetas: List? = null, - emojis: List? = null, - isDraft: Boolean, - onReady: (ChatMessageEvent) -> Unit, - ) { - val tags = TagArrayBuilder() - to?.forEach { tags.add(arrayOf("p", it)) } - replyTos?.forEach { tags.add(arrayOf("e", it, "", "reply")) } - mentions?.forEach { tags.add(arrayOf("p", it, "", "mention")) } - zapReceiver?.forEach { tags.add(ZapSplitSetupSerializer.toTagArray(it)) } - zapRaiserAmount?.let { tags.add(ZapRaiserSerializer.toTagArray(it)) } - - if (markAsSensitive) { - tags.add(ContentWarningSerializer.toTagArray()) - } - geohash?.let { tags.addAll(geohashMipMap(it)) } - subject?.let { tags.subject(subject) } - imetas?.forEach { - tags.add(Nip92MediaAttachments.createTag(it)) - } - emojis?.forEach { tags.add(it.toTagArray()) } - // tags.add(AltTagSerializer.toTagArray(ALT)) - - if (isDraft) { - signer.assembleRumor(createdAt, KIND, tags.build(), msg, onReady) - } else { - signer.sign(createdAt, KIND, tags.build(), msg, onReady) - } - } - } -} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/NIP17Factory.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/NIP17Factory.kt index a85a05c75..ac156bb89 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/NIP17Factory.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/NIP17Factory.kt @@ -20,16 +20,17 @@ */ package com.vitorpamplona.quartz.nip17Dm +import com.vitorpamplona.quartz.nip01Core.EventHintBundle import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent +import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent -import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrl -import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetup -import com.vitorpamplona.quartz.nip59Giftwrap.GiftWrapEvent -import com.vitorpamplona.quartz.nip59Giftwrap.SealedRumorEvent -import com.vitorpamplona.quartz.nip92IMeta.IMetaTag -import com.vitorpamplona.quartz.nip94FileMetadata.Dimension +import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag +import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent +import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent class NIP17Factory { data class Result( @@ -76,131 +77,51 @@ class NIP17Factory { recursiveGiftWrapCreation(event, to.toList(), signer, wraps, onReady) } - fun createMsgNIP17( - msg: String, - to: List, + fun createMessageNIP17( + template: EventTemplate, signer: NostrSigner, - subject: String? = null, - replyTos: List? = null, - mentions: List? = null, - zapReceiver: List? = null, - markAsSensitive: Boolean = false, - zapRaiserAmount: Long? = null, - geohash: String? = null, - imetas: List? = null, - emojis: List? = null, - draftTag: String? = null, onReady: (Result) -> Unit, ) { - val senderPublicKey = signer.pubKey - - ChatMessageEvent.create( - msg = msg, - to = to, - signer = signer, - subject = subject, - replyTos = replyTos, - mentions = mentions, - zapReceiver = zapReceiver, - markAsSensitive = markAsSensitive, - zapRaiserAmount = zapRaiserAmount, - geohash = geohash, - isDraft = draftTag != null, - imetas = imetas, - emojis = emojis, - ) { senderMessage -> - if (draftTag != null) { + signer.sign(template) { senderMessage -> + createWraps(senderMessage, senderMessage.groupMembers(), signer) { wraps -> onReady( Result( msg = senderMessage, - wraps = listOf(), + wraps = wraps, ), ) - } else { - createWraps(senderMessage, to.plus(senderPublicKey).toSet(), signer) { wraps -> - onReady( - Result( - msg = senderMessage, - wraps = wraps, - ), - ) - } } } } fun createEncryptedFileNIP17( - url: String, - to: List, - repliesToHex: List? = null, - contentType: String?, - algo: String, - key: ByteArray, - nonce: ByteArray? = null, - originalHash: String? = null, - hash: String? = null, - size: Int? = null, - dimensions: Dimension? = null, - blurhash: String? = null, - sensitiveContent: Boolean? = null, - alt: String?, - draftTag: String? = null, + template: EventTemplate, signer: NostrSigner, onReady: (Result) -> Unit, ) { - val senderPublicKey = signer.pubKey - - ChatMessageEncryptedFileHeaderEvent.create( - url = url, - to = to, - repliesTo = repliesToHex, - contentType = contentType, - algo = algo, - key = key, - nonce = nonce, - originalHash = originalHash, - hash = hash, - size = size, - dimensions = dimensions, - blurhash = blurhash, - sensitiveContent = sensitiveContent, - alt = alt, - signer = signer, - isDraft = draftTag != null, - ) { senderMessage -> - if (draftTag != null) { + signer.sign(template) { senderMessage -> + createWraps(senderMessage, senderMessage.groupMembers(), signer) { wraps -> onReady( Result( msg = senderMessage, - wraps = listOf(), + wraps = wraps, ), ) - } else { - createWraps(senderMessage, to.plus(senderPublicKey).toSet(), signer) { wraps -> - onReady( - Result( - msg = senderMessage, - wraps = wraps, - ), - ) - } } } } fun createReactionWithinGroup( content: String, - originalNote: Event, + originalNote: EventHintBundle, to: List, signer: NostrSigner, onReady: (Result) -> Unit, ) { val senderPublicKey = signer.pubKey - ReactionEvent.create( - content, - originalNote, - signer, + signer.sign( + ReactionEvent.build(content, originalNote), ) { senderReaction -> createWraps(senderReaction, to.plus(senderPublicKey).toSet(), signer) { wraps -> onReady( @@ -214,18 +135,16 @@ class NIP17Factory { } fun createReactionWithinGroup( - emojiUrl: EmojiUrl, - originalNote: Event, + emojiUrl: EmojiUrlTag, + originalNote: EventHintBundle, to: List, signer: NostrSigner, onReady: (Result) -> Unit, ) { val senderPublicKey = signer.pubKey - ReactionEvent.create( - emojiUrl, - originalNote, - signer, + signer.sign( + ReactionEvent.build(emojiUrl, originalNote), ) { senderReaction -> createWraps(senderReaction, to.plus(senderPublicKey).toSet(), signer) { wraps -> onReady( diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/base/BaseDMGroupEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/base/BaseDMGroupEvent.kt new file mode 100644 index 000000000..8ec33b140 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/base/BaseDMGroupEvent.kt @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip17Dm.base + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.nip59Giftwrap.WrappedEvent +import kotlinx.collections.immutable.toImmutableSet + +@Immutable +open class BaseDMGroupEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + kind: Int, + tags: Array>, + content: String, + sig: HexKey, +) : WrappedEvent(id, pubKey, createdAt, kind, tags, content, sig), + ChatroomKeyable, + NIP17Group { + /** Recipients intended to receive this conversation */ + fun recipients() = tags.mapNotNull(PTag::parse) + + /** Recipients intended to receive this conversation */ + fun recipientsPubKey() = tags.mapNotNull(PTag::parseKey) + + fun talkingWith(oneSideHex: String): Set { + val listedPubKeys = recipientsPubKey() + + val result = + if (pubKey == oneSideHex) { + listedPubKeys.toSet().minus(oneSideHex) + } else { + listedPubKeys.plus(pubKey).toSet().minus(oneSideHex) + } + + if (result.isEmpty()) { + // talking to myself + return setOf(pubKey) + } + + return result + } + + override fun groupMembers() = recipientsPubKey().plus(pubKey).toSet() + + override fun chatroomKey(toRemove: String): ChatroomKey = ChatroomKey(talkingWith(toRemove).toImmutableSet()) +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/ChatroomKey.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/base/ChatroomKey.kt similarity index 96% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/ChatroomKey.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/base/ChatroomKey.kt index fbd6d98d8..4d6da3fe0 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/ChatroomKey.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/base/ChatroomKey.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip17Dm +package com.vitorpamplona.quartz.nip17Dm.base import androidx.compose.runtime.Stable import com.vitorpamplona.quartz.nip01Core.HexKey diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/ChatroomKeyable.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/base/ChatroomKeyable.kt similarity index 96% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/ChatroomKeyable.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/base/ChatroomKeyable.kt index 1d5ddf635..ad92949f3 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/ChatroomKeyable.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/base/ChatroomKeyable.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip17Dm +package com.vitorpamplona.quartz.nip17Dm.base import com.vitorpamplona.quartz.nip01Core.HexKey diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/NIP17Group.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/base/NIP17Group.kt similarity index 96% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/NIP17Group.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/base/NIP17Group.kt index e49b925ee..a422a3ee0 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/NIP17Group.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/base/NIP17Group.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip17Dm +package com.vitorpamplona.quartz.nip17Dm.base import com.vitorpamplona.quartz.nip01Core.HexKey diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/files/ChatMessageEncryptedFileHeaderEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/files/ChatMessageEncryptedFileHeaderEvent.kt new file mode 100644 index 000000000..35fda3241 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/files/ChatMessageEncryptedFileHeaderEvent.kt @@ -0,0 +1,118 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip17Dm.files + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.EventHintBundle +import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.nip17Dm.base.BaseDMGroupEvent +import com.vitorpamplona.quartz.nip17Dm.files.encryption.AESGCM +import com.vitorpamplona.quartz.nip17Dm.files.tags.EncryptionAlgo +import com.vitorpamplona.quartz.nip17Dm.files.tags.EncryptionKey +import com.vitorpamplona.quartz.nip17Dm.files.tags.EncryptionNonce +import com.vitorpamplona.quartz.nip17Dm.files.tags.FileTypeTag +import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent +import com.vitorpamplona.quartz.nip31Alts.alt +import com.vitorpamplona.quartz.nip94FileMetadata.tags.BlurhashTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.HashTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.OriginalHashTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.SizeTag +import com.vitorpamplona.quartz.utils.TimeUtils + +@Immutable +class ChatMessageEncryptedFileHeaderEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : BaseDMGroupEvent(id, pubKey, createdAt, KIND, tags, content, sig) { + fun replyTo() = tags.mapNotNull(ETag::parseId) + + fun url() = content + + fun mimeType() = tags.firstNotNullOfOrNull(FileTypeTag::parse) + + fun hash() = tags.firstNotNullOfOrNull(HashTag::parse) + + fun size() = tags.firstNotNullOfOrNull(SizeTag::parse) + + fun dimensions() = tags.firstNotNullOfOrNull(DimensionTag::parse) + + fun blurhash() = tags.firstNotNullOfOrNull(BlurhashTag::parse) + + fun originalHash() = tags.firstNotNullOfOrNull(OriginalHashTag::parse) + + fun algo() = tags.firstNotNullOfOrNull(EncryptionAlgo::parse) + + fun key() = tags.firstNotNullOfOrNull(EncryptionKey::parse) + + fun nonce() = tags.firstNotNullOfOrNull(EncryptionNonce::parse) + + companion object { + const val KIND = 15 + const val ALT_DESCRIPTION = "Encrypted file in chat" + + fun build( + to: List, + url: String, + cipher: AESGCM, + replyTo: EventHintBundle? = null, + mimeType: String? = null, + hash: String? = null, + size: Int? = null, + dimension: DimensionTag? = null, + blurhash: String? = null, + originalHash: String? = null, + magnetUri: String? = null, + torrentInfoHash: String? = null, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, url, createdAt) { + alt(ALT_DESCRIPTION) + + group(to) + + encryptionAlgo(cipher.name()) + encryptionKey(cipher.keyBytes) + encryptionNonce(cipher.nonce) + + replyTo?.let { reply(replyTo) } + + hash?.let { hash(it) } + size?.let { fileSize(it) } + mimeType?.let { mimeType(it) } + dimension?.let { dimension(it) } + blurhash?.let { blurhash(it) } + originalHash?.let { originalHash(it) } + magnetUri?.let { magnet(it) } + torrentInfoHash?.let { torrentInfohash(it) } + + initializer() + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/files/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/files/TagArrayBuilderExt.kt new file mode 100644 index 000000000..4f003cffa --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/files/TagArrayBuilderExt.kt @@ -0,0 +1,86 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip17Dm.files + +import com.vitorpamplona.quartz.nip01Core.EventHintBundle +import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.nip01Core.tags.people.pTag +import com.vitorpamplona.quartz.nip01Core.tags.people.pTags +import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag +import com.vitorpamplona.quartz.nip17Dm.files.tags.EncryptionAlgo +import com.vitorpamplona.quartz.nip17Dm.files.tags.EncryptionKey +import com.vitorpamplona.quartz.nip17Dm.files.tags.EncryptionNonce +import com.vitorpamplona.quartz.nip17Dm.files.tags.FileTypeTag +import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent +import com.vitorpamplona.quartz.nip94FileMetadata.tags.BlurhashTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.FallbackTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.HashTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.ImageTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.MagnetTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.OriginalHashTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.ServiceTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.SizeTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.SummaryTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.ThumbTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.TorrentInfoHash + +fun TagArrayBuilder.reply(msg: MarkedETag) = add(msg.toTagArray()) + +fun TagArrayBuilder.reply(msg: EventHintBundle) = reply(msg.toMarkedETag(MarkedETag.MARKER.REPLY)) + +fun TagArrayBuilder.group(list: List) = pTags(list) + +fun TagArrayBuilder.group(pubkey: PTag) = pTag(pubkey) + +fun TagArrayBuilder.encryptionAlgo(algo: String) = add(EncryptionAlgo.assemble(algo)) + +fun TagArrayBuilder.encryptionKey(key: ByteArray) = add(EncryptionKey.assemble(key)) + +fun TagArrayBuilder.encryptionNonce(nonce: ByteArray) = add(EncryptionNonce.assemble(nonce)) + +fun TagArrayBuilder.mimeType(mimeType: String) = add(FileTypeTag.assemble(mimeType)) + +fun TagArrayBuilder.hash(hash: HexKey) = add(HashTag.assemble(hash)) + +fun TagArrayBuilder.fileSize(size: Int) = add(SizeTag.assemble(size)) + +fun TagArrayBuilder.dimension(dim: DimensionTag) = add(DimensionTag.assemble(dim)) + +fun TagArrayBuilder.blurhash(blurhash: String) = add(BlurhashTag.assemble(blurhash)) + +fun TagArrayBuilder.originalHash(hash: HexKey) = add(OriginalHashTag.assemble(hash)) + +fun TagArrayBuilder.torrentInfohash(hash: String) = add(TorrentInfoHash.assemble(hash)) + +fun TagArrayBuilder.magnet(magnetUri: String) = add(MagnetTag.assemble(magnetUri)) + +fun TagArrayBuilder.image(imageUrl: HexKey) = add(ImageTag.assemble(imageUrl)) + +fun TagArrayBuilder.thumb(trumbUrl: HexKey) = add(ThumbTag.assemble(trumbUrl)) + +fun TagArrayBuilder.summary(summary: HexKey) = add(SummaryTag.assemble(summary)) + +fun TagArrayBuilder.fallback(fallbackUrl: HexKey) = add(FallbackTag.assemble(fallbackUrl)) + +fun TagArrayBuilder.service(service: HexKey) = add(ServiceTag.assemble(service)) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/AESGCM.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/files/encryption/AESGCM.kt similarity index 97% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/AESGCM.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/files/encryption/AESGCM.kt index 84572f733..8f3c7541f 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/AESGCM.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/files/encryption/AESGCM.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip17Dm +package com.vitorpamplona.quartz.nip17Dm.files.encryption import com.vitorpamplona.quartz.CryptoUtils import com.vitorpamplona.quartz.nip01Core.toHexKey diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/NostrCipher.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/files/encryption/NostrCipher.kt similarity index 95% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/NostrCipher.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/files/encryption/NostrCipher.kt index 6a90da014..0931f0396 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/NostrCipher.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/files/encryption/NostrCipher.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip17Dm +package com.vitorpamplona.quartz.nip17Dm.files.encryption interface NostrCipher { fun name(): String diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/files/tags/EncryptionAlgo.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/files/tags/EncryptionAlgo.kt new file mode 100644 index 000000000..a44727038 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/files/tags/EncryptionAlgo.kt @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip17Dm.files.tags + +class EncryptionAlgo { + companion object { + const val TAG_NAME = "encryption-algorithm" + const val TAG_SIZE = 2 + + @JvmStatic + fun isTag(tag: Array) = tag.size >= TAG_SIZE && tag[0] == TAG_NAME + + @JvmStatic + fun parse(tag: Array): String? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1] + } + + @JvmStatic + fun assemble(excerpt: String) = arrayOf(TAG_NAME, excerpt) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/files/tags/EncryptionKey.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/files/tags/EncryptionKey.kt new file mode 100644 index 000000000..4b57b1bf9 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/files/tags/EncryptionKey.kt @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip17Dm.files.tags + +import com.vitorpamplona.quartz.nip01Core.hexToByteArray +import com.vitorpamplona.quartz.nip01Core.toHexKey + +class EncryptionKey { + companion object { + const val TAG_NAME = "decryption-key" + const val TAG_SIZE = 2 + + @JvmStatic + fun isTag(tag: Array) = tag.size >= TAG_SIZE && tag[0] == TAG_NAME + + @JvmStatic + fun parse(tag: Array): ByteArray? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return runCatching { tag[1].hexToByteArray() }.getOrNull() + } + + @JvmStatic + fun assemble(key: ByteArray) = arrayOf(TAG_NAME, key.toHexKey()) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/files/tags/EncryptionNonce.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/files/tags/EncryptionNonce.kt new file mode 100644 index 000000000..41c051ad1 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/files/tags/EncryptionNonce.kt @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip17Dm.files.tags + +import com.vitorpamplona.quartz.nip01Core.hexToByteArray +import com.vitorpamplona.quartz.nip01Core.toHexKey + +class EncryptionNonce { + companion object { + const val TAG_NAME = "decryption-nonce" + const val TAG_SIZE = 2 + + @JvmStatic + fun isTag(tag: Array) = tag.size >= TAG_SIZE && tag[0] == TAG_NAME + + @JvmStatic + fun parse(tag: Array): ByteArray? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return runCatching { tag[1].hexToByteArray() }.getOrNull() + } + + @JvmStatic + fun assemble(nonce: ByteArray) = arrayOf(EncryptionKey.TAG_NAME, nonce.toHexKey()) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/files/tags/FileTypeTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/files/tags/FileTypeTag.kt new file mode 100644 index 000000000..38620fd2d --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/files/tags/FileTypeTag.kt @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip17Dm.files.tags + +class FileTypeTag { + companion object { + const val TAG_NAME = "file-type" + const val TAG_SIZE = 2 + + fun isIn( + tag: Array, + mimeTypes: Set, + ) = tag.size >= TAG_SIZE && tag[0] == TAG_NAME && tag[1] in mimeTypes + + @JvmStatic + fun parse(tag: Array): String? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1] + } + + @JvmStatic + fun assemble(mimeType: String) = arrayOf(TAG_NAME, mimeType) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip30CustomEmoji/EmojiPackEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/messages/ChatMessageEvent.kt similarity index 50% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip30CustomEmoji/EmojiPackEvent.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/messages/ChatMessageEvent.kt index db83d02c9..680d0ab30 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip30CustomEmoji/EmojiPackEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/messages/ChatMessageEvent.kt @@ -18,69 +18,55 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip30CustomEmoji +package com.vitorpamplona.quartz.nip17Dm.messages import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.EventHintBundle import com.vitorpamplona.quartz.nip01Core.HexKey -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer -import com.vitorpamplona.quartz.nip51Lists.GeneralListEvent +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.nip17Dm.base.BaseDMGroupEvent +import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent.Companion.ALT_DESCRIPTION +import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.utils.TimeUtils @Immutable -class EmojiPackEvent( +class ChatMessageEvent( id: HexKey, pubKey: HexKey, createdAt: Long, tags: Array>, content: String, sig: HexKey, -) : GeneralListEvent(id, pubKey, createdAt, KIND, tags, content, sig) { - companion object { - const val KIND = 30030 - const val ALT = "Emoji pack" +) : BaseDMGroupEvent(id, pubKey, createdAt, KIND, tags, content, sig) { + fun replyTo() = tags.mapNotNull(ETag::parseId) - fun create( - name: String = "", - signer: NostrSigner, + companion object { + const val KIND = 14 + const val ALT = "Direct message" + + fun build( + msg: String, + to: List, createdAt: Long = TimeUtils.now(), - onReady: (EmojiPackEvent) -> Unit, - ) { - val content = "" + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, msg, createdAt) { + alt(ALT_DESCRIPTION) + group(to) + initializer() + } - val tags = mutableListOf>() - tags.add(arrayOf("d", name)) - tags.add(AltTagSerializer.toTagArray(ALT)) - - signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady) + fun reply( + msg: String, + reply: EventHintBundle, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, msg, createdAt) { + alt(ALT_DESCRIPTION) + group((reply.event.recipients() + reply.toPTag()).distinctBy { it.pubKey }) + initializer() } } } - -@Immutable -data class EmojiUrl( - val code: String, - val url: String, -) { - fun encode(): String = ":$code:$url" - - fun toTagArray() = arrayOf("emoji", code, url) - - companion object { - fun decode(encodedEmojiSetup: String): EmojiUrl? { - val emojiParts = encodedEmojiSetup.split(":", limit = 3) - return if (emojiParts.size > 2) { - EmojiUrl(emojiParts[1], emojiParts[2]) - } else { - null - } - } - - fun parse(tag: Array): EmojiUrl? = - if (tag.size > 2 && tag[0] == "emoji") { - EmojiUrl(tag[1], tag[2]) - } else { - null - } - } -} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/messages/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/messages/TagArrayBuilderExt.kt new file mode 100644 index 000000000..c81a70808 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/messages/TagArrayBuilderExt.kt @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip17Dm.messages + +import com.vitorpamplona.quartz.nip01Core.EventHintBundle +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.nip01Core.tags.people.pTag +import com.vitorpamplona.quartz.nip01Core.tags.people.pTags +import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag +import com.vitorpamplona.quartz.nip14Subject.SubjectTag + +fun TagArrayBuilder.reply(msg: MarkedETag) = add(msg.toTagArray()) + +fun TagArrayBuilder.reply(msg: EventHintBundle) = reply(msg.toMarkedETag(MarkedETag.MARKER.REPLY)) + +fun TagArrayBuilder.group(list: List) = pTags(list) + +fun TagArrayBuilder.group(pubkey: PTag) = pTag(pubkey) + +fun TagArrayBuilder.changeSubject(newSubject: String) = addUnique(SubjectTag.assemble(newSubject)) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/ChatMessageRelayListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/settings/ChatMessageRelayListEvent.kt similarity index 95% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/ChatMessageRelayListEvent.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/settings/ChatMessageRelayListEvent.kt index 8b0993421..b6177171a 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/ChatMessageRelayListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/settings/ChatMessageRelayListEvent.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip17Dm +package com.vitorpamplona.quartz.nip17Dm.settings import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.HexKey @@ -26,7 +26,7 @@ import com.vitorpamplona.quartz.nip01Core.core.BaseReplaceableEvent import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer +import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.utils.TimeUtils @Immutable @@ -58,7 +58,7 @@ class ChatMessageRelayListEvent( relays .map { arrayOf("relay", it) - }.plusElement(AltTagSerializer.toTagArray("Relay list to receive private messages")) + }.plusElement(AltTag.assemble("Relay list to receive private messages")) .toTypedArray() fun updateRelayList( diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/GenericRepostEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/GenericRepostEvent.kt index 124d678b0..08e331d57 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/GenericRepostEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/GenericRepostEvent.kt @@ -24,8 +24,17 @@ import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.addressables.aTag +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag +import com.vitorpamplona.quartz.nip01Core.tags.events.eTag +import com.vitorpamplona.quartz.nip01Core.tags.kinds.kind +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.nip01Core.tags.people.pTag +import com.vitorpamplona.quartz.nip31Alts.AltTag +import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.utils.TimeUtils @Immutable @@ -52,6 +61,25 @@ class GenericRepostEvent( const val KIND = 16 const val ALT = "Generic repost" + fun build( + boostedPost: Event, + eventSourceRelay: String?, + authorHomeRelay: String?, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, boostedPost.toJson(), createdAt) { + alt(ALT) + + kind(boostedPost.kind) + pTag(PTag(boostedPost.pubKey, authorHomeRelay)) + eTag(ETag(boostedPost.id, eventSourceRelay, boostedPost.pubKey)) + if (boostedPost is AddressableEvent) { + aTag(boostedPost.aTag(eventSourceRelay)) + } + + initializer() + } + fun create( boostedPost: Event, signer: NostrSigner, @@ -67,11 +95,11 @@ class GenericRepostEvent( ) if (boostedPost is AddressableEvent) { - tags.add(arrayOf("a", boostedPost.address().toTag())) + tags.add(arrayOf("a", boostedPost.aTag().toTag())) } tags.add(arrayOf("k", "${boostedPost.kind}")) - tags.add(AltTagSerializer.toTagArray(ALT)) + tags.add(AltTag.assemble(ALT)) signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/RepostEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/RepostEvent.kt index 332ab407b..db25dc36d 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/RepostEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/RepostEvent.kt @@ -24,10 +24,17 @@ import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.addressables.aTag +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag +import com.vitorpamplona.quartz.nip01Core.tags.events.eTag import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEvents +import com.vitorpamplona.quartz.nip01Core.tags.kinds.kind +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.nip01Core.tags.people.pTag import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUsers -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer +import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.utils.TimeUtils @Immutable @@ -54,26 +61,23 @@ class RepostEvent( const val KIND = 6 const val ALT = "Repost event" - fun create( + fun build( boostedPost: Event, - signer: NostrSigner, + eventSourceRelay: String?, + authorHomeRelay: String?, createdAt: Long = TimeUtils.now(), - onReady: (RepostEvent) -> Unit, - ) { - val content = boostedPost.toJson() - - val replyToPost = arrayOf("e", boostedPost.id) - val replyToAuthor = arrayOf("p", boostedPost.pubKey) - - var tags: Array> = arrayOf(replyToPost, replyToAuthor) + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, boostedPost.toJson(), createdAt) { + alt(ALT) + pTag(PTag(boostedPost.pubKey, authorHomeRelay)) + eTag(ETag(boostedPost.id, eventSourceRelay, boostedPost.pubKey)) if (boostedPost is AddressableEvent) { - tags += listOf(arrayOf("a", boostedPost.address().toTag())) + aTag(boostedPost.aTag(eventSourceRelay)) } + kind(boostedPost.kind) - tags += listOf(AltTagSerializer.toTagArray(ALT)) - - signer.sign(createdAt, KIND, tags, content, onReady) + initializer() } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/EntityExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/EntityExt.kt new file mode 100644 index 000000000..eb2d964f5 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/EntityExt.kt @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip18Reposts.quotes + +import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent +import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress +import com.vitorpamplona.quartz.nip19Bech32.entities.NEmbed +import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent +import com.vitorpamplona.quartz.nip19Bech32.entities.Note + +fun Note.toQuoteTag() = QEventTag(hex, null, null) + +fun NEvent.toQuoteTag() = QEventTag(hex, relay.firstOrNull(), author) + +fun NAddress.toQuoteTag() = QAddressableTag(kind, author, dTag, relay.firstOrNull()) + +fun NEmbed.toQuoteTag() = + if (event is AddressableEvent) { + QAddressableTag(event.kind, event.pubKey, event.dTag(), null) + } else { + QEventTag(event.id, null, event.pubKey) + } + +fun Note.toQuoteTagArray() = QEventTag.assemble(hex, null, null) + +fun NEvent.toQuoteTagArray() = QEventTag.assemble(hex, relay.firstOrNull(), author) + +fun NAddress.toQuoteTagArray() = QAddressableTag.assemble(kind, author, dTag, relay.firstOrNull()) + +fun NEmbed.toQuoteTagArray() = + if (event is AddressableEvent) { + QAddressableTag.assemble(event.kind, event.pubKey, event.dTag(), null) + } else { + QEventTag.assemble(event.id, null, event.pubKey) + } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/EventExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/EventExt.kt new file mode 100644 index 000000000..2f7169c7e --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/EventExt.kt @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip18Reposts.quotes + +import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.Event + +fun Event.forEachTaggedQuoteId(onEach: (eventId: HexKey) -> Unit) = tags.forEachTaggedQuoteId(onEach) + +fun Event.mapTaggedQuoteId(map: (eventId: HexKey) -> R) = tags.mapTaggedQuoteId(map) + +fun Event.taggedQuotes() = tags.taggedQuotes() + +fun Event.taggedQuoteIds() = tags.taggedQuoteIds() + +fun Event.firstTaggedQuote() = tags.firstTaggedQuote() + +fun Event.isTaggedQuote(idHex: String) = tags.isTaggedQuote(idHex) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/QAddressableTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/QAddressableTag.kt new file mode 100644 index 000000000..f4ddd5bd9 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/QAddressableTag.kt @@ -0,0 +1,83 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip18Reposts.quotes + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address +import com.vitorpamplona.quartz.utils.arrayOfNotNull +import com.vitorpamplona.quartz.utils.bytesUsedInMemory +import com.vitorpamplona.quartz.utils.pointerSizeInBytes + +@Immutable +data class QAddressableTag( + val address: Address, +) : QTag { + var relay: String? = null + + constructor( + address: Address, + relayHint: String?, + ) : this(address) { + this.relay = relayHint + } + + constructor( + kind: Int, + pubKeyHex: HexKey, + dTag: String, + relayHint: String?, + ) : this(Address(kind, pubKeyHex, dTag)) { + this.relay = relayHint + } + + fun countMemory(): Long = + 2 * pointerSizeInBytes + + address.countMemory() + + (relay?.bytesUsedInMemory() ?: 0) + + override fun toTagArray() = assemble(address, relay) + + companion object { + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): QAddressableTag? { + if (tag.size < TAG_SIZE || tag[0] != QTag.TAG_NAME) return null + val address = Address.parse(tag[1]) ?: return null + return QAddressableTag(address, tag.getOrNull(2)) + } + + @JvmStatic + fun assemble( + kind: Int, + pubKeyHex: HexKey, + dTag: String, + relay: String?, + ) = arrayOfNotNull(QTag.TAG_NAME, Address.assemble(kind, pubKeyHex, dTag), relay) + + @JvmStatic + fun assemble( + address: Address, + relay: String?, + ) = arrayOfNotNull(QTag.TAG_NAME, address.toValue(), relay) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/QEventTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/QEventTag.kt new file mode 100644 index 000000000..864eaf14a --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/QEventTag.kt @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip18Reposts.quotes + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.utils.arrayOfNotNull +import com.vitorpamplona.quartz.utils.bytesUsedInMemory +import com.vitorpamplona.quartz.utils.pointerSizeInBytes + +@Immutable +data class QEventTag( + val eventId: HexKey, +) : QTag { + var relay: String? = null + var author: HexKey? = null + + constructor(eventId: HexKey, relayHint: String? = null, authorPubKeyHex: HexKey? = null) : this(eventId) { + this.relay = relayHint + this.author = authorPubKeyHex + } + + fun countMemory(): Long = + 3 * pointerSizeInBytes + // 3 fields, 4 bytes each reference (32bit) + eventId.bytesUsedInMemory() + + (relay?.bytesUsedInMemory() ?: 0) + + (author?.bytesUsedInMemory() ?: 0) + + override fun toTagArray() = assemble(eventId, relay, author) + + companion object { + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): QEventTag? { + if (tag.size < TAG_SIZE || tag[0] != QTag.TAG_NAME) return null + return QEventTag(tag[1], tag.getOrNull(2), tag.getOrNull(3)) + } + + @JvmStatic + fun assemble( + eventId: HexKey, + relay: String?, + author: HexKey?, + ) = arrayOfNotNull(QTag.TAG_NAME, eventId, relay, author) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/QTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/QTag.kt new file mode 100644 index 000000000..f600ecf2e --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/QTag.kt @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip18Reposts.quotes + +interface QTag { + fun toTagArray(): Array + + companion object { + const val TAG_NAME = "q" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): QTag? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return if (tag[1].length == 64) { + QEventTag.parse(tag) + } else { + QAddressableTag.parse(tag) + } + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/TagArrayBuilderExt.kt new file mode 100644 index 000000000..0017af7e0 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/TagArrayBuilderExt.kt @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip18Reposts.quotes + +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.tags.people.toQuoteTagArray +import com.vitorpamplona.quartz.nip19Bech32.entities.Entity +import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress +import com.vitorpamplona.quartz.nip19Bech32.entities.NEmbed +import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent +import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile +import com.vitorpamplona.quartz.nip19Bech32.entities.NPub +import com.vitorpamplona.quartz.nip19Bech32.entities.Note + +fun TagArrayBuilder.quote(tag: QTag) = add(tag.toTagArray()) + +fun TagArrayBuilder.quotes(tag: List) = addAll(tag.map { it.toTagArray() }) + +fun TagArrayBuilder.quote(entity: Entity) = + when (entity) { + is Note -> add(entity.toQuoteTagArray()) + is NEvent -> add(entity.toQuoteTagArray()) + is NAddress -> add(entity.toQuoteTagArray()) + is NEmbed -> add(entity.toQuoteTagArray()) + is NPub -> add(entity.toQuoteTagArray()) + is NProfile -> add(entity.toQuoteTagArray()) + else -> { + this + } + } + +fun TagArrayBuilder.quotes(entities: List) = entities.forEach { quote(it) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/TagArrayExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/TagArrayExt.kt new file mode 100644 index 000000000..bd0d2c193 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/TagArrayExt.kt @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip18Reposts.quotes + +import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArray +import com.vitorpamplona.quartz.nip01Core.core.forEachTagged +import com.vitorpamplona.quartz.nip01Core.core.isTagged +import com.vitorpamplona.quartz.nip01Core.core.mapValueTagged +import com.vitorpamplona.quartz.nip01Core.core.mapValues + +fun TagArray.forEachTaggedQuoteId(onEach: (eventId: HexKey) -> Unit) = this.forEachTagged(QTag.TAG_NAME, onEach) + +fun TagArray.mapTaggedQuoteId(map: (eventId: HexKey) -> R) = this.mapValueTagged(QTag.TAG_NAME, map) + +fun TagArray.taggedQuotes() = this.mapNotNull(QTag.Companion::parse) + +fun TagArray.taggedQuoteIds() = this.mapValues(QTag.TAG_NAME) + +fun TagArray.firstTaggedQuote() = this.firstNotNullOfOrNull(QTag.Companion::parse) + +fun TagArray.isTaggedQuote(idHex: String) = this.isTagged(QTag.TAG_NAME, idHex) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/ATagExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/ATagExt.kt index 2ad157784..67f471930 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/ATagExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/ATagExt.kt @@ -27,7 +27,7 @@ import com.vitorpamplona.quartz.utils.Hex fun ATag.Companion.isATag(key: String): Boolean = key.startsWith("naddr1") || key.contains(":") -fun ATag.Companion.parse( +fun ATag.Companion.parseAny( address: String, relay: String?, ): ATag? = diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/Nip19Parser.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/Nip19Parser.kt index 6d24c4c1a..1222d013f 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/Nip19Parser.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/Nip19Parser.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.quartz.nip19Bech32 import android.util.Log import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.HexKey -import com.vitorpamplona.quartz.nip01Core.KeyPair +import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.hexToByteArray import com.vitorpamplona.quartz.nip01Core.toHexKey import com.vitorpamplona.quartz.nip19Bech32.bech32.bechToBytes diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/entities/NProfile.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/entities/NProfile.kt index c4ceb3bf3..c31746b68 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/entities/NProfile.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/entities/NProfile.kt @@ -51,12 +51,12 @@ data class NProfile( fun create( authorPubKeyHex: String, - relay: List, + relays: List, ): String = TlvBuilder() .apply { addHex(TlvTypes.SPECIAL, authorPubKeyHex) - relay.forEach { + relays.forEach { addStringIfNotNull(TlvTypes.RELAY, it) } }.build() diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip21UriScheme/EventExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip21UriScheme/EventExt.kt index 5e7d3e93e..25909ec22 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip21UriScheme/EventExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip21UriScheme/EventExt.kt @@ -20,7 +20,10 @@ */ package com.vitorpamplona.quartz.nip21UriScheme +import com.vitorpamplona.quartz.nip01Core.EventHintBundle import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip19Bech32.toNIP19 fun Event.toNostrUri(): String = "nostr:${toNIP19()}" + +fun EventHintBundle.toNostrUri(): String = "nostr:${toNEvent()}" diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/CommentEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/CommentEvent.kt index 1f56efc18..bc62dec53 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/CommentEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/CommentEvent.kt @@ -25,26 +25,24 @@ import com.vitorpamplona.quartz.nip01Core.EventHintBundle import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag -import com.vitorpamplona.quartz.nip01Core.tags.events.ETag -import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohashMipMap -import com.vitorpamplona.quartz.nip01Core.tags.hashtags.buildHashtagTags -import com.vitorpamplona.quartz.nip10Notes.BaseTextNoteEvent -import com.vitorpamplona.quartz.nip10Notes.PTag -import com.vitorpamplona.quartz.nip10Notes.content.buildUrlRefs -import com.vitorpamplona.quartz.nip10Notes.content.findHashtags -import com.vitorpamplona.quartz.nip10Notes.content.findURLs -import com.vitorpamplona.quartz.nip19Bech32.parseAtag -import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrl -import com.vitorpamplona.quartz.nip36SensitiveContent.ContentWarningSerializer -import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetup -import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetupSerializer -import com.vitorpamplona.quartz.nip57Zaps.zapraiser.ZapRaiserSerializer -import com.vitorpamplona.quartz.nip92IMeta.IMetaTag -import com.vitorpamplona.quartz.nip92IMeta.Nip92MediaAttachments +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent +import com.vitorpamplona.quartz.nip21UriScheme.toNostrUri +import com.vitorpamplona.quartz.nip22Comments.tags.ReplyAddressTag +import com.vitorpamplona.quartz.nip22Comments.tags.ReplyAuthorTag +import com.vitorpamplona.quartz.nip22Comments.tags.ReplyEventTag +import com.vitorpamplona.quartz.nip22Comments.tags.ReplyIdentifierTag +import com.vitorpamplona.quartz.nip22Comments.tags.ReplyKindTag +import com.vitorpamplona.quartz.nip22Comments.tags.RootAddressTag +import com.vitorpamplona.quartz.nip22Comments.tags.RootAuthorTag +import com.vitorpamplona.quartz.nip22Comments.tags.RootEventTag +import com.vitorpamplona.quartz.nip22Comments.tags.RootIdentifierTag +import com.vitorpamplona.quartz.nip22Comments.tags.RootKindTag +import com.vitorpamplona.quartz.nip31Alts.alt +import com.vitorpamplona.quartz.nip73ExternalIds.ExternalId import com.vitorpamplona.quartz.utils.TimeUtils -import com.vitorpamplona.quartz.utils.removeTrailingNullsAndEmptyOthers +import com.vitorpamplona.quartz.utils.lastNotNullOfOrNull @Immutable class CommentEvent( @@ -54,17 +52,23 @@ class CommentEvent( tags: Array>, content: String, sig: HexKey, -) : BaseTextNoteEvent(id, pubKey, createdAt, KIND, tags, content, sig), +) : BaseThreadedEvent(id, pubKey, createdAt, KIND, tags, content, sig), RootScope { - fun root() = tags.firstOrNull { it.size > 3 && it[3] == "root" }?.get(1) + fun rootAuthor() = tags.firstNotNullOfOrNull(RootAuthorTag::parse) - fun getRootScopes() = tags.filter { it.size > 1 && it[0] == "I" || it[0] == "A" || it[0] == "E" } + fun replyAuthor() = tags.firstNotNullOfOrNull(ReplyAuthorTag::parse) - fun getRootKinds() = tags.filter { it.size > 1 && it[0] == "K" } + fun rootAuthors() = tags.filter(RootAuthorTag::match) - fun getDirectReplies() = tags.filter { it.size > 1 && it[0] == "i" || it[0] == "a" || it[0] == "e" } + fun replyAuthors() = tags.filter(ReplyAuthorTag::match) - fun getDirectKinds() = tags.filter { it.size > 1 && it[0] == "k" } + fun rootScopes() = tags.filter { RootIdentifierTag.match(it) || RootAddressTag.match(it) || RootEventTag.match(it) } + + fun rootKinds() = tags.filter(RootKindTag::match) + + fun directReplies() = tags.filter { ReplyIdentifierTag.match(it) || ReplyAddressTag.match(it) || ReplyEventTag.match(it) } + + fun directKinds() = tags.filter(ReplyKindTag::match) fun isGeohashTag(tag: Array) = tag.size > 1 && (tag[0] == "i" || tag[0] == "I") && tag[1].startsWith("geo:") @@ -80,157 +84,71 @@ class CommentEvent( fun isTaggedGeoHashes(hashtags: Set) = geohashes().any { it in hashtags } - override fun markedReplyTos(): List = tags.filter { it.size > 1 && it[0] == "e" }.map { it[1] } + tags.filter { it.size > 1 && it[0] == "E" }.map { it[1] } + override fun markedReplyTos(): List = + tags.mapNotNull(ReplyEventTag::parseKey) + + tags.mapNotNull(RootEventTag::parseKey) - override fun unMarkedReplyTos() = emptyList() + override fun unmarkedReplyTos() = emptyList() override fun replyingTo(): HexKey? = - tags.lastOrNull { it.size > 1 && it[0] == "e" }?.get(1) - ?: tags.lastOrNull { it.size > 1 && it[0] == "E" }?.get(1) + tags.lastNotNullOfOrNull(ReplyEventTag::parseKey) + ?: tags.lastNotNullOfOrNull(RootEventTag::parseKey) - override fun replyingToAddress(): ATag? = - tags.lastOrNull { it.size > 1 && it[0] == "a" }?.let { ATag.parseAtag(it[1], it.getOrNull(2)) } - ?: tags.lastOrNull { it.size > 1 && it[0] == "A" }?.let { ATag.parseAtag(it[1], it.getOrNull(2)) } + fun replyingToAddressId(): String? = + tags.lastNotNullOfOrNull(RootAddressTag::parseAddress) + ?: tags.lastNotNullOfOrNull(ReplyAddressTag::parseAddress) - override fun replyingToAddressOrEvent(): HexKey? = replyingToAddress()?.toTag() ?: replyingTo() + override fun replyingToAddressOrEvent(): HexKey? = replyingToAddressId() ?: replyingTo() companion object { const val KIND = 1111 + const val ALT = "Reply to " - fun rootGeohashMipMap(geohash: String): Array> = - geohash.indices - .asSequence() - .map { arrayOf("I", "geo:" + geohash.substring(0, it + 1)) } - .toList() - .reversed() - .toTypedArray() - - fun firstReplyToEvent( + fun replyBuilder( msg: String, replyingTo: EventHintBundle, - usersMentioned: Set = emptySet(), - addressesMentioned: Set = emptySet(), - eventsMentioned: Set = emptySet(), - imetas: List? = null, - emojis: List? = null, - geohash: String? = null, - zapReceiver: List? = null, - markAsSensitive: Boolean = false, - zapRaiserAmount: Long? = null, - isDraft: Boolean, - signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (CommentEvent) -> Unit, - ) { - val tags = mutableListOf>() + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, msg, createdAt) { + alt(ALT + replyingTo.toNostrUri()) - if (replyingTo.event is AddressableEvent) { - tags.add(removeTrailingNullsAndEmptyOthers("A", replyingTo.event.addressTag(), replyingTo.relay)) - tags.add(removeTrailingNullsAndEmptyOthers("a", replyingTo.event.addressTag(), replyingTo.relay)) - } - - tags.add(removeTrailingNullsAndEmptyOthers("E", replyingTo.event.id, replyingTo.relay, replyingTo.event.pubKey)) - tags.add(arrayOf("K", "${replyingTo.event.kind}")) - - tags.add(removeTrailingNullsAndEmptyOthers("e", replyingTo.event.id, replyingTo.relay, replyingTo.event.pubKey)) - tags.add(arrayOf("k", "${replyingTo.event.kind}")) - - create(msg, tags, usersMentioned, addressesMentioned, eventsMentioned, imetas, emojis, geohash, zapReceiver, markAsSensitive, zapRaiserAmount, isDraft, signer, createdAt, onReady) - } - - fun replyComment( - msg: String, - replyingTo: EventHintBundle, - usersMentioned: Set = emptySet(), - addressesMentioned: Set = emptySet(), - eventsMentioned: Set = emptySet(), - imetas: List? = null, - emojis: List? = null, - geohash: String? = null, - zapReceiver: List? = null, - markAsSensitive: Boolean = false, - zapRaiserAmount: Long? = null, - isDraft: Boolean, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (CommentEvent) -> Unit, - ) { - val tags = mutableListOf>() - - tags.addAll(replyingTo.event.getRootScopes()) - tags.addAll(replyingTo.event.getRootKinds()) - - tags.add(removeTrailingNullsAndEmptyOthers("e", replyingTo.event.id, replyingTo.relay, replyingTo.event.pubKey)) - tags.add(arrayOf("k", "${replyingTo.event.kind}")) - - create(msg, tags, usersMentioned, addressesMentioned, eventsMentioned, imetas, emojis, geohash, zapReceiver, markAsSensitive, zapRaiserAmount, isDraft, signer, createdAt, onReady) - } - - fun createGeoComment( - msg: String, - geohash: String? = null, - usersMentioned: Set = emptySet(), - addressesMentioned: Set = emptySet(), - eventsMentioned: Set = emptySet(), - imetas: List? = null, - emojis: List? = null, - zapReceiver: List? = null, - markAsSensitive: Boolean = false, - zapRaiserAmount: Long? = null, - isDraft: Boolean, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (CommentEvent) -> Unit, - ) { - val tags = mutableListOf>() - geohash?.let { tags.addAll(rootGeohashMipMap(it)) } - tags.add(arrayOf("K", "geo")) - - create(msg, tags, usersMentioned, addressesMentioned, eventsMentioned, imetas, emojis, null, zapReceiver, markAsSensitive, zapRaiserAmount, isDraft, signer, createdAt, onReady) - } - - private fun create( - msg: String, - tags: MutableList>, - usersMentioned: Set = emptySet(), - addressesMentioned: Set = emptySet(), - eventsMentioned: Set = emptySet(), - imetas: List? = null, - emojis: List? = null, - geohash: String? = null, - zapReceiver: List? = null, - markAsSensitive: Boolean = false, - zapRaiserAmount: Long? = null, - isDraft: Boolean, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (CommentEvent) -> Unit, - ) { - usersMentioned.forEach { tags.add(it.toPTagArray()) } - addressesMentioned.forEach { tags.add(it.toQTagArray()) } - eventsMentioned.forEach { tags.add(it.toQTagArray()) } - - tags.addAll(buildHashtagTags(findHashtags(msg))) - tags.addAll(buildUrlRefs(findURLs(msg))) - - emojis?.forEach { tags.add(it.toTagArray()) } - - zapReceiver?.forEach { tags.add(ZapSplitSetupSerializer.toTagArray(it)) } - zapRaiserAmount?.let { tags.add(ZapRaiserSerializer.toTagArray(it)) } - - if (markAsSensitive) { - tags.add(ContentWarningSerializer.toTagArray()) - } - geohash?.let { tags.addAll(geohashMipMap(it)) } - imetas?.forEach { - tags.add(Nip92MediaAttachments.createTag(it)) - } - - if (isDraft) { - signer.assembleRumor(createdAt, KIND, tags.toTypedArray(), msg, onReady) + if (replyingTo.event is CommentEvent) { + addAll(replyingTo.event.rootScopes()) + addAll(replyingTo.event.rootKinds()) + addAll(replyingTo.event.rootAuthors()) } else { - signer.sign(createdAt, KIND, tags.toTypedArray(), msg, onReady) + if (replyingTo.event is AddressableEvent) { + rootAddress(replyingTo.event.addressTag(), replyingTo.relay) + replyAddress(replyingTo.event.addressTag(), replyingTo.relay) + } + + rootEvent(replyingTo.event.id, replyingTo.relay, replyingTo.event.pubKey) + rootKind(replyingTo.event.kind) + rootAuthor(replyingTo.event.pubKey, replyingTo.authorHomeRelay) } + + replyEvent(replyingTo.event.id, replyingTo.relay, replyingTo.event.pubKey) + replyKind(replyingTo.event.kind) + replyAuthor(replyingTo.event.pubKey, replyingTo.authorHomeRelay) + + initializer() + } + + fun replyExternalIdentity( + msg: String, + extId: ExternalId, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, msg, createdAt) { + alt(ALT + extId.toScope()) + + rootExternalIdentity(extId) + rootKind(extId) + + replyExternalIdentity(extId) + replyKind(extId) + + initializer() } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/TagArrayBuilderExt.kt new file mode 100644 index 000000000..a5b5d2ebe --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/TagArrayBuilderExt.kt @@ -0,0 +1,90 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip22Comments + +import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.nip01Core.tags.people.pTag +import com.vitorpamplona.quartz.nip01Core.tags.people.pTags +import com.vitorpamplona.quartz.nip22Comments.tags.ReplyAddressTag +import com.vitorpamplona.quartz.nip22Comments.tags.ReplyAuthorTag +import com.vitorpamplona.quartz.nip22Comments.tags.ReplyEventTag +import com.vitorpamplona.quartz.nip22Comments.tags.ReplyIdentifierTag +import com.vitorpamplona.quartz.nip22Comments.tags.ReplyKindTag +import com.vitorpamplona.quartz.nip22Comments.tags.RootAddressTag +import com.vitorpamplona.quartz.nip22Comments.tags.RootAuthorTag +import com.vitorpamplona.quartz.nip22Comments.tags.RootEventTag +import com.vitorpamplona.quartz.nip22Comments.tags.RootIdentifierTag +import com.vitorpamplona.quartz.nip22Comments.tags.RootKindTag +import com.vitorpamplona.quartz.nip73ExternalIds.ExternalId + +fun TagArrayBuilder.rootAddress( + addressId: String, + relayHint: String?, +) = addUnique(RootAddressTag.assemble(addressId, relayHint)) + +fun TagArrayBuilder.rootEvent( + eventId: String, + relayHint: String?, + pubkey: String?, +) = addUnique(RootEventTag.assemble(eventId, relayHint, pubkey)) + +fun TagArrayBuilder.rootExternalIdentity(id: ExternalId) = addAll(RootIdentifierTag.assemble(id)) + +fun TagArrayBuilder.rootKind(kind: String) = addUnique(RootKindTag.assemble(kind)) + +fun TagArrayBuilder.rootKind(kind: Int) = addUnique(RootKindTag.assemble(kind)) + +fun TagArrayBuilder.rootKind(id: ExternalId) = addUnique(RootKindTag.assemble(id)) + +fun TagArrayBuilder.rootAuthor( + pubKey: HexKey, + relay: String?, +) = add(RootAuthorTag.assemble(pubKey, relay)) + +fun TagArrayBuilder.replyAddress( + addressId: String, + relayHint: String?, +) = addUnique(ReplyAddressTag.assemble(addressId, relayHint)) + +fun TagArrayBuilder.replyEvent( + eventId: String, + relayHint: String?, + pubkey: String?, +) = addUnique(ReplyEventTag.assemble(eventId, relayHint, pubkey)) + +fun TagArrayBuilder.replyExternalIdentity(id: ExternalId) = addAll(ReplyIdentifierTag.assemble(id)) + +fun TagArrayBuilder.replyKind(kind: String) = addUnique(ReplyKindTag.assemble(kind)) + +fun TagArrayBuilder.replyKind(kind: Int) = addUnique(ReplyKindTag.assemble(kind)) + +fun TagArrayBuilder.replyKind(id: ExternalId) = addUnique(RootKindTag.assemble(id)) + +fun TagArrayBuilder.replyAuthor( + pubKey: HexKey, + relay: String?, +) = add(ReplyAuthorTag.assemble(pubKey, relay)) + +fun TagArrayBuilder.notify(list: List) = pTags(list) + +fun TagArrayBuilder.notify(pubkey: PTag) = pTag(pubkey) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/ReplyAddressTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/ReplyAddressTag.kt new file mode 100644 index 000000000..b80a558bf --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/ReplyAddressTag.kt @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip22Comments.tags + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.Tag +import com.vitorpamplona.quartz.nip01Core.core.match +import com.vitorpamplona.quartz.nip01Core.core.valueIfMatches +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address +import com.vitorpamplona.quartz.nip01Core.tags.addressables.AddressReference +import com.vitorpamplona.quartz.utils.arrayOfNotNull + +@Immutable +class ReplyAddressTag( + val ref: AddressReference, +) { + constructor(aTagId: String, relayHint: String?) : this(AddressReference(aTagId, relayHint)) + + fun toTagArray() = assemble(ref.addressId, ref.relay) + + companion object { + const val TAG_NAME = "a" + const val TAG_SIZE = 2 + + @JvmStatic + fun match(tag: Tag) = tag.match(TAG_NAME, TAG_SIZE) + + @JvmStatic + fun parse(tag: Array): ReplyAddressTag? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return ReplyAddressTag(tag[1], tag.getOrNull(2)) + } + + @JvmStatic + fun parseAddress(tag: Array) = tag.valueIfMatches(TAG_NAME, TAG_SIZE) + + @JvmStatic + fun assemble( + addressId: HexKey, + relay: String?, + ) = arrayOfNotNull(TAG_NAME, addressId, relay) + + @JvmStatic + fun assemble( + kind: Int, + pubKey: String, + dTag: String, + relay: String?, + ) = assemble(Address.assemble(kind, pubKey, dTag), relay) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/ReplyAuthorTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/ReplyAuthorTag.kt new file mode 100644 index 000000000..0b42d207b --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/ReplyAuthorTag.kt @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip22Comments.tags + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.Tag +import com.vitorpamplona.quartz.nip01Core.core.match +import com.vitorpamplona.quartz.nip01Core.tags.people.PubKeyReferenceTag +import com.vitorpamplona.quartz.utils.arrayOfNotNull + +@Immutable +data class ReplyAuthorTag( + override val pubKey: HexKey, + override val relayHint: String? = null, +) : PubKeyReferenceTag { + fun toTagArray() = assemble(pubKey, relayHint) + + companion object { + const val TAG_NAME = "p" + const val TAG_SIZE = 2 + + @JvmStatic + fun match(tag: Tag) = tag.match(TAG_NAME, TAG_SIZE) + + @JvmStatic + fun parse(tag: Tag): ReplyAuthorTag? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + if (tag[1].length != 64) return null + return ReplyAuthorTag(tag[1], tag.getOrNull(2)) + } + + @JvmStatic + fun parseKey(tag: Tag): HexKey? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + if (tag[1].length != 64) return null + return tag[1] + } + + @JvmStatic + fun assemble( + pubkey: HexKey, + relayHint: String?, + ) = arrayOfNotNull(TAG_NAME, pubkey, relayHint) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/ReplyEventTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/ReplyEventTag.kt new file mode 100644 index 000000000..26926372f --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/ReplyEventTag.kt @@ -0,0 +1,84 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip22Comments.tags + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.Tag +import com.vitorpamplona.quartz.nip01Core.core.match +import com.vitorpamplona.quartz.nip01Core.core.valueIfMatches +import com.vitorpamplona.quartz.nip01Core.tags.events.EventReference +import com.vitorpamplona.quartz.utils.arrayOfNotNull + +@Immutable +class ReplyEventTag( + val ref: EventReference, +) { + constructor(eventId: String, relayHint: String?, pubkey: String?) : this(EventReference(eventId, relayHint, pubkey)) + + fun toTagArray() = assemble(ref) + + companion object { + const val TAG_NAME = "e" + const val TAG_SIZE = 2 + + @JvmStatic + fun match(tag: Tag) = tag.match(TAG_NAME, TAG_SIZE) + + @JvmStatic + fun isTagged( + tag: Array, + eventId: String, + ) = tag.match(TAG_NAME, eventId, TAG_SIZE) + + @JvmStatic + fun isIn( + tag: Array, + eventIds: Set, + ) = tag.match(TAG_NAME, eventIds, TAG_SIZE) + + @JvmStatic + fun parse(tag: Array): ReplyEventTag? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return ReplyEventTag(tag[1], tag.getOrNull(2), tag.getOrNull(3)) + } + + @JvmStatic + fun parseKey(tag: Array) = tag.valueIfMatches(TAG_NAME, TAG_SIZE) + + @JvmStatic + fun parseValidKey(tag: Array): String? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + if (tag[1].length != 64) return null + return tag[1] + } + + @JvmStatic + fun assemble( + eventId: HexKey, + relay: String?, + pubkey: String?, + ) = arrayOfNotNull(TAG_NAME, eventId, relay, pubkey) + + @JvmStatic + fun assemble(ref: EventReference) = assemble(ref.eventId, ref.relayHint, ref.author) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/ReplyIdentifierTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/ReplyIdentifierTag.kt new file mode 100644 index 000000000..9926fa8ff --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/ReplyIdentifierTag.kt @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip22Comments.tags + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.core.Tag +import com.vitorpamplona.quartz.nip01Core.core.match +import com.vitorpamplona.quartz.nip01Core.core.valueIfMatches +import com.vitorpamplona.quartz.nip01Core.tags.geohash.GeoHash +import com.vitorpamplona.quartz.nip73ExternalIds.ExternalId +import com.vitorpamplona.quartz.nip73ExternalIds.GeohashId +import com.vitorpamplona.quartz.utils.arrayOfNotNull + +@Immutable +class ReplyIdentifierTag { + companion object { + const val TAG_NAME = "i" + const val TAG_SIZE = 2 + + @JvmStatic + fun match(tag: Tag) = tag.match(TAG_NAME, TAG_SIZE) + + @JvmStatic + fun parse(tag: Tag) = tag.valueIfMatches(TAG_NAME, TAG_SIZE) + + @JvmStatic + fun assemble( + identity: String, + hint: String?, + ) = arrayOfNotNull(TAG_NAME, identity, hint) + + @JvmStatic + fun assemble(id: ExternalId): List> = + when (id) { + is GeohashId -> GeoHash.geoMipMap(id.geohash).map { assemble(it, id.hint) } + else -> listOf(assemble(id.toScope(), id.hint())) + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/ReplyKindTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/ReplyKindTag.kt new file mode 100644 index 000000000..f3a0028ed --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/ReplyKindTag.kt @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip22Comments.tags + +import com.vitorpamplona.quartz.nip01Core.core.Tag +import com.vitorpamplona.quartz.nip01Core.core.match +import com.vitorpamplona.quartz.nip01Core.core.matchAndHasValue +import com.vitorpamplona.quartz.nip01Core.core.valueToIntIfMatches +import com.vitorpamplona.quartz.nip73ExternalIds.ExternalId + +class ReplyKindTag { + companion object { + const val TAG_NAME = "k" + const val TAG_SIZE = 2 + + @JvmStatic + fun match(tag: Tag) = tag.match(TAG_NAME, TAG_SIZE) + + @JvmStatic + fun isTagged(tag: Tag) = tag.matchAndHasValue(TAG_NAME, TAG_SIZE) + + @JvmStatic + fun isTagged( + tag: Tag, + kind: String, + ) = tag.match(TAG_NAME, kind, TAG_SIZE) + + @JvmStatic + fun isIn( + tag: Tag, + kinds: Set, + ) = tag.match(TAG_NAME, kinds, TAG_SIZE) + + @JvmStatic + fun parse(tag: Tag) = tag.valueToIntIfMatches(TAG_NAME, TAG_SIZE) + + @JvmStatic + fun assemble(kind: String) = arrayOf(TAG_NAME, kind) + + @JvmStatic + fun assemble(kind: Int) = arrayOf(TAG_NAME, kind.toString()) + + @JvmStatic + fun assemble(id: ExternalId) = RootKindTag.assemble(id.toKind()) + + @JvmStatic + fun assemble(kinds: List): List = kinds.map { assemble(it) } + + @JvmStatic + fun assemble(kinds: Set): List = kinds.map { assemble(it) } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/RootAddressTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/RootAddressTag.kt new file mode 100644 index 000000000..2d035639f --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/RootAddressTag.kt @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip22Comments.tags + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.Tag +import com.vitorpamplona.quartz.nip01Core.core.match +import com.vitorpamplona.quartz.nip01Core.core.valueIfMatches +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address +import com.vitorpamplona.quartz.nip01Core.tags.addressables.AddressReference +import com.vitorpamplona.quartz.utils.arrayOfNotNull + +@Immutable +class RootAddressTag( + val ref: AddressReference, +) { + constructor(aTagId: String, relayHint: String?) : this(AddressReference(aTagId, relayHint)) + + fun toTagArray() = assemble(ref.addressId, ref.relay) + + companion object { + const val TAG_NAME = "A" + const val TAG_SIZE = 2 + + @JvmStatic + fun match(tag: Tag) = tag.match(TAG_NAME, TAG_SIZE) + + @JvmStatic + fun isTagged( + tag: Array, + addressId: String, + ) = tag.match(TAG_NAME, addressId, TAG_SIZE) + + @JvmStatic + fun isIn( + tag: Array, + addressIds: Set, + ) = tag.match(TAG_NAME, addressIds, TAG_SIZE) + + @JvmStatic + fun parse(tag: Array): RootAddressTag? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return RootAddressTag(tag[1], tag.getOrNull(2)) + } + + @JvmStatic + fun parseValidAddress(tag: Array): String? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return Address.parse(tag[1])?.toValue() + } + + @JvmStatic + fun parseAddress(tag: Array) = tag.valueIfMatches(TAG_NAME, TAG_SIZE) + + @JvmStatic + fun assemble( + addressId: HexKey, + relay: String?, + ) = arrayOfNotNull(TAG_NAME, addressId, relay) + + @JvmStatic + fun assemble( + kind: Int, + pubKey: String, + dTag: String, + relay: String?, + ) = assemble(Address.assemble(kind, pubKey, dTag), relay) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/RootAuthorTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/RootAuthorTag.kt new file mode 100644 index 000000000..5b6d44c6f --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/RootAuthorTag.kt @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip22Comments.tags + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.Tag +import com.vitorpamplona.quartz.nip01Core.core.match +import com.vitorpamplona.quartz.nip01Core.tags.people.PubKeyReferenceTag +import com.vitorpamplona.quartz.utils.arrayOfNotNull + +@Immutable +data class RootAuthorTag( + override val pubKey: HexKey, + override val relayHint: String? = null, +) : PubKeyReferenceTag { + fun toTagArray() = assemble(pubKey, relayHint) + + companion object { + const val TAG_NAME = "P" + const val TAG_SIZE = 2 + + @JvmStatic + fun match(tag: Tag) = tag.match(TAG_NAME, TAG_SIZE) + + @JvmStatic + fun parse(tag: Tag): ReplyAuthorTag? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + if (tag[1].length != 64) return null + return ReplyAuthorTag(tag[1], tag.getOrNull(2)) + } + + @JvmStatic + fun parseKey(tag: Tag): HexKey? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + if (tag[1].length != 64) return null + return tag[1] + } + + @JvmStatic + fun assemble( + pubkey: HexKey, + relayHint: String?, + ) = arrayOfNotNull(TAG_NAME, pubkey, relayHint) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/RootEventTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/RootEventTag.kt new file mode 100644 index 000000000..c26282cc4 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/RootEventTag.kt @@ -0,0 +1,84 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip22Comments.tags + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.Tag +import com.vitorpamplona.quartz.nip01Core.core.match +import com.vitorpamplona.quartz.nip01Core.core.valueIfMatches +import com.vitorpamplona.quartz.nip01Core.tags.events.EventReference +import com.vitorpamplona.quartz.utils.arrayOfNotNull + +@Immutable +class RootEventTag( + val ref: EventReference, +) { + constructor(eventId: String, relayHint: String?, pubkey: String?) : this(EventReference(eventId, relayHint, pubkey)) + + fun toTagArray() = assemble(ref) + + companion object { + const val TAG_NAME = "E" + const val TAG_SIZE = 2 + + @JvmStatic + fun match(tag: Tag) = tag.match(TAG_NAME, TAG_SIZE) + + @JvmStatic + fun isTagged( + tag: Array, + eventId: String, + ) = tag.match(TAG_NAME, eventId, TAG_SIZE) + + @JvmStatic + fun isIn( + tag: Array, + eventIds: Set, + ) = tag.match(TAG_NAME, eventIds, TAG_SIZE) + + @JvmStatic + fun parse(tag: Array): RootEventTag? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return RootEventTag(tag[1], tag.getOrNull(2), tag.getOrNull(3)) + } + + @JvmStatic + fun parseKey(tag: Array) = tag.valueIfMatches(TAG_NAME, TAG_SIZE) + + @JvmStatic + fun parseValidKey(tag: Array): String? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + if (tag[1].length != 64) return null + return tag[1] + } + + @JvmStatic + fun assemble( + eventId: HexKey, + relay: String?, + pubkey: String?, + ) = arrayOfNotNull(TAG_NAME, eventId, relay, pubkey) + + @JvmStatic + fun assemble(ref: EventReference) = assemble(ref.eventId, ref.relayHint, ref.author) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/RootIdentifierTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/RootIdentifierTag.kt new file mode 100644 index 000000000..34aa37a9d --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/RootIdentifierTag.kt @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip22Comments.tags + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.core.Tag +import com.vitorpamplona.quartz.nip01Core.core.match +import com.vitorpamplona.quartz.nip01Core.core.valueIfMatches +import com.vitorpamplona.quartz.nip01Core.tags.geohash.GeoHash +import com.vitorpamplona.quartz.nip73ExternalIds.ExternalId +import com.vitorpamplona.quartz.nip73ExternalIds.GeohashId +import com.vitorpamplona.quartz.utils.arrayOfNotNull + +@Immutable +class RootIdentifierTag { + companion object { + const val TAG_NAME = "I" + const val TAG_SIZE = 2 + + @JvmStatic + fun match(tag: Tag) = tag.match(TAG_NAME, TAG_SIZE) + + @JvmStatic + fun parse(tag: Tag) = tag.valueIfMatches(TAG_NAME, TAG_SIZE) + + @JvmStatic + fun assemble( + identity: String, + hint: String?, + ) = arrayOfNotNull(TAG_NAME, identity, hint) + + @JvmStatic + fun assemble(id: ExternalId): List> = + when (id) { + is GeohashId -> GeoHash.geoMipMap(id.geohash).map { assemble(it, id.hint) } + else -> listOf(assemble(id.toScope(), id.hint())) + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/RootKindTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/RootKindTag.kt new file mode 100644 index 000000000..bbbe5861d --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/RootKindTag.kt @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip22Comments.tags + +import com.vitorpamplona.quartz.nip01Core.core.Tag +import com.vitorpamplona.quartz.nip01Core.core.match +import com.vitorpamplona.quartz.nip01Core.core.valueIfMatches +import com.vitorpamplona.quartz.nip73ExternalIds.ExternalId + +class RootKindTag { + companion object { + const val TAG_NAME = "K" + const val TAG_SIZE = 2 + + @JvmStatic + fun match(tag: Tag) = tag.match(TAG_NAME, TAG_SIZE) + + @JvmStatic + fun parse(tag: Tag) = tag.valueIfMatches(TAG_NAME, TAG_SIZE) + + @JvmStatic + fun assemble(kind: String): Tag = arrayOf(TAG_NAME, kind) + + @JvmStatic + fun assemble(kind: Int): Tag = assemble(kind.toString()) + + @JvmStatic + fun assemble(id: ExternalId) = assemble(id.toKind()) + + @JvmStatic + fun assemble(kinds: List): List = kinds.map { assemble(it) } + + @JvmStatic + fun assemble(kinds: Set): List = kinds.map { assemble(it) } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip23LongContent/LongTextNoteEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip23LongContent/LongTextNoteEvent.kt index 38dc910b9..fe75d9034 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip23LongContent/LongTextNoteEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip23LongContent/LongTextNoteEvent.kt @@ -23,13 +23,20 @@ package com.vitorpamplona.quartz.nip23LongContent import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags -import com.vitorpamplona.quartz.nip10Notes.BaseTextNoteEvent -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer +import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent +import com.vitorpamplona.quartz.nip23LongContent.tags.ImageTag +import com.vitorpamplona.quartz.nip23LongContent.tags.PublishedAtTag +import com.vitorpamplona.quartz.nip23LongContent.tags.SummaryTag +import com.vitorpamplona.quartz.nip23LongContent.tags.TitleTag +import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.utils.TimeUtils +import java.util.UUID @Immutable class LongTextNoteEvent( @@ -39,47 +46,48 @@ class LongTextNoteEvent( tags: Array>, content: String, sig: HexKey, -) : BaseTextNoteEvent(id, pubKey, createdAt, KIND, tags, content, sig), +) : BaseThreadedEvent(id, pubKey, createdAt, KIND, tags, content, sig), AddressableEvent { override fun dTag() = tags.dTag() - override fun address(relayHint: String?) = ATag(kind, pubKey, dTag(), relayHint) + override fun aTag(relayHint: String?) = ATag(kind, pubKey, dTag(), relayHint) + + override fun address() = Address(kind, pubKey, dTag()) override fun addressTag() = ATag.assembleATagId(kind, pubKey, dTag()) fun topics() = hashtags() - fun title() = tags.firstOrNull { it.size > 1 && it[0] == "title" }?.get(1) + fun title() = tags.firstNotNullOfOrNull(TitleTag::parse) - fun image() = tags.firstOrNull { it.size > 1 && it[0] == "image" }?.get(1) + fun image() = tags.firstNotNullOfOrNull(ImageTag::parse) - fun summary() = tags.firstOrNull { it.size > 1 && it[0] == "summary" }?.get(1) + fun summary() = tags.firstNotNullOfOrNull(SummaryTag::parse) - fun publishedAt() = - try { - tags.firstOrNull { it.size > 1 && it[0] == "published_at" }?.get(1)?.toLongOrNull() - } catch (_: Exception) { - null - } + fun publishedAt() = tags.firstNotNullOfOrNull(PublishedAtTag::parse) companion object { const val KIND = 30023 - fun create( - msg: String, - title: String?, - replyTos: List?, - mentions: List?, - signer: NostrSigner, + fun build( + description: String, + title: String, + summary: String? = null, + image: String? = null, + publishedAt: Long? = null, + dTag: String = UUID.randomUUID().toString(), createdAt: Long = TimeUtils.now(), - onReady: (LongTextNoteEvent) -> Unit, - ) { - val tags = mutableListOf>() - replyTos?.forEach { tags.add(arrayOf("e", it)) } - mentions?.forEach { tags.add(arrayOf("p", it)) } - title?.let { tags.add(arrayOf("title", it)) } - tags.add(AltTagSerializer.toTagArray("Blog post: $title")) - signer.sign(createdAt, KIND, tags.toTypedArray(), msg, onReady) + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, description, createdAt) { + dTag(dTag) + alt("Blog post: $title") + + title(title) + summary?.let { summary(it) } + image?.let { image(it) } + publishedAt?.let { publishedAt(it) } + + initializer() } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip23LongContent/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip23LongContent/TagArrayBuilderExt.kt new file mode 100644 index 000000000..bcbbf782c --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip23LongContent/TagArrayBuilderExt.kt @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip23LongContent + +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip23LongContent.tags.ImageTag +import com.vitorpamplona.quartz.nip23LongContent.tags.PublishedAtTag +import com.vitorpamplona.quartz.nip23LongContent.tags.SummaryTag +import com.vitorpamplona.quartz.nip23LongContent.tags.TitleTag + +fun TagArrayBuilder.title(title: String) = addUnique(TitleTag.assemble(title)) + +fun TagArrayBuilder.summary(summary: String) = addUnique(SummaryTag.assemble(summary)) + +fun TagArrayBuilder.image(imageUrl: String) = addUnique(ImageTag.assemble(imageUrl)) + +fun TagArrayBuilder.publishedAt(publishedAt: Long) = addUnique(PublishedAtTag.assemble(publishedAt)) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip23LongContent/tags/ImageTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip23LongContent/tags/ImageTag.kt new file mode 100644 index 000000000..d7e74286e --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip23LongContent/tags/ImageTag.kt @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip23LongContent.tags + +class ImageTag { + companion object { + const val TAG_NAME = "image" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): String? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1] + } + + @JvmStatic + fun assemble(url: String) = arrayOf(TAG_NAME, url) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip23LongContent/tags/PublishedAtTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip23LongContent/tags/PublishedAtTag.kt new file mode 100644 index 000000000..671348f27 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip23LongContent/tags/PublishedAtTag.kt @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip23LongContent.tags + +class PublishedAtTag { + companion object { + const val TAG_NAME = "published_at" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): Long? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1].toLongOrNull() + } + + @JvmStatic + fun assemble(timestamp: Long) = arrayOf(TAG_NAME, timestamp.toString()) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip23LongContent/tags/SummaryTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip23LongContent/tags/SummaryTag.kt new file mode 100644 index 000000000..1e04d3b56 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip23LongContent/tags/SummaryTag.kt @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip23LongContent.tags + +class SummaryTag { + companion object { + const val TAG_NAME = "summary" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): String? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1] + } + + @JvmStatic + fun assemble(title: String) = arrayOf(TAG_NAME, title) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip23LongContent/tags/TitleTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip23LongContent/tags/TitleTag.kt new file mode 100644 index 000000000..409c4eb1d --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip23LongContent/tags/TitleTag.kt @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip23LongContent.tags + +class TitleTag { + companion object { + const val TAG_NAME = "title" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): String? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1] + } + + @JvmStatic + fun assemble(title: String) = arrayOf(TAG_NAME, title) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip25Reactions/ReactionEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip25Reactions/ReactionEvent.kt index d4abf954c..2b7ef491f 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip25Reactions/ReactionEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip25Reactions/ReactionEvent.kt @@ -21,11 +21,19 @@ package com.vitorpamplona.quartz.nip25Reactions import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.EventHintBundle import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrl +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.addressables.aTag +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag +import com.vitorpamplona.quartz.nip01Core.tags.events.eTag +import com.vitorpamplona.quartz.nip01Core.tags.kinds.kind +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.nip01Core.tags.people.pTag +import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag +import com.vitorpamplona.quartz.nip30CustomEmoji.emoji import com.vitorpamplona.quartz.utils.TimeUtils @Immutable @@ -37,68 +45,50 @@ class ReactionEvent( content: String, sig: HexKey, ) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { - fun originalPost() = tags.filter { it.size > 1 && it[0] == "e" }.map { it[1] } + fun originalPost() = tags.mapNotNull(ETag::parseId) - fun originalAuthor() = tags.filter { it.size > 1 && it[0] == "p" }.map { it[1] } + fun originalAuthor() = tags.mapNotNull(PTag::parseKey) companion object { const val KIND = 7 + const val LIKE = "+" + const val DISLIKE = "-" - fun createWarning( - originalNote: Event, - signer: NostrSigner, + fun like( + reactedTo: EventHintBundle, createdAt: Long = TimeUtils.now(), - onReady: (ReactionEvent) -> Unit, - ) = create("\u26A0\uFE0F", originalNote, signer, createdAt, onReady) + ) = build(LIKE, reactedTo, createdAt) - fun createLike( - originalNote: Event, - signer: NostrSigner, + fun dislike( + reactedTo: EventHintBundle, createdAt: Long = TimeUtils.now(), - onReady: (ReactionEvent) -> Unit, - ) = create("+", originalNote, signer, createdAt, onReady) + ) = build(DISLIKE, reactedTo, createdAt) - fun create( - content: String, - originalNote: Event, - signer: NostrSigner, + fun build( + reaction: String, + reactedTo: EventHintBundle, createdAt: Long = TimeUtils.now(), - onReady: (ReactionEvent) -> Unit, - ) { - var tags = - listOf( - arrayOf("e", originalNote.id), - arrayOf("p", originalNote.pubKey), - arrayOf("k", originalNote.kind.toString()), - ) - if (originalNote is AddressableEvent) { - tags = tags + listOf(arrayOf("a", originalNote.address().toTag())) + ) = eventTemplate(KIND, reaction, createdAt) { + eTag(reactedTo.toETag()) + if (reactedTo.event is AddressableEvent) { + aTag(reactedTo.event.aTag(reactedTo.relay)) } - - return signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady) + pTag(reactedTo.event.pubKey, reactedTo.relay) + kind(reactedTo.event.kind) } - fun create( - emojiUrl: EmojiUrl, - originalNote: Event, - signer: NostrSigner, + fun build( + reaction: EmojiUrlTag, + reactedTo: EventHintBundle, createdAt: Long = TimeUtils.now(), - onReady: (ReactionEvent) -> Unit, - ) { - val content = ":${emojiUrl.code}:" - - var tags = - arrayOf( - arrayOf("e", originalNote.id), - arrayOf("p", originalNote.pubKey), - arrayOf("emoji", emojiUrl.code, emojiUrl.url), - ) - - if (originalNote is AddressableEvent) { - tags += arrayOf(arrayOf("a", originalNote.address().toTag())) + ) = eventTemplate(KIND, reaction.toContentEncode(), createdAt) { + eTag(reactedTo.toETag()) + if (reactedTo.event is AddressableEvent) { + aTag(reactedTo.event.aTag(reactedTo.relay)) } - - signer.sign(createdAt, KIND, tags, content, onReady) + pTag(reactedTo.event.pubKey, reactedTo.relay) + kind(reactedTo.event.kind) + emoji(reaction) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/ChannelCreateEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/ChannelCreateEvent.kt deleted file mode 100644 index 717f79293..000000000 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/ChannelCreateEvent.kt +++ /dev/null @@ -1,103 +0,0 @@ -/** - * Copyright (c) 2024 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.quartz.nip28PublicChat - -import android.util.Log -import androidx.compose.runtime.Immutable -import com.fasterxml.jackson.module.kotlin.readValue -import com.vitorpamplona.quartz.nip01Core.HexKey -import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer -import com.vitorpamplona.quartz.utils.TimeUtils - -@Immutable -class ChannelCreateEvent( - id: HexKey, - pubKey: HexKey, - createdAt: Long, - tags: Array>, - content: String, - sig: HexKey, -) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { - fun channelInfo(): ChannelData = - try { - EventMapper.mapper.readValue(content) - } catch (e: Exception) { - Log.e("ChannelMetadataEvent", "Can't parse channel info $content", e) - ChannelData(null, null, null) - } - - companion object { - const val KIND = 40 - - fun create( - name: String?, - about: String?, - picture: String?, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (ChannelCreateEvent) -> Unit, - ) = create( - ChannelData( - name, - about, - picture, - ), - signer, - createdAt, - onReady, - ) - - fun create( - channelInfo: ChannelData?, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (ChannelCreateEvent) -> Unit, - ) { - val content = - try { - if (channelInfo != null) { - EventMapper.mapper.writeValueAsString(channelInfo) - } else { - "" - } - } catch (t: Throwable) { - Log.e("ChannelCreateEvent", "Couldn't parse channel information", t) - "" - } - - val tags = - arrayOf( - AltTagSerializer.toTagArray("Public chat creation event ${channelInfo?.name?.let { "about $it" }}"), - ) - - signer.sign(createdAt, KIND, tags, content, onReady) - } - } - - @Immutable data class ChannelData( - val name: String?, - val about: String?, - val picture: String?, - ) -} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/ChannelListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/ChannelListEvent.kt index 4eef28c7d..14c89f45c 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/ChannelListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/ChannelListEvent.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.quartz.nip28PublicChat import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer +import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.nip51Lists.GeneralListEvent import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.bytesUsedInMemory @@ -262,7 +262,7 @@ class ChannelListEvent( if (tags.any { it.size > 1 && it[0] == "alt" }) { tags } else { - tags + AltTagSerializer.toTagArray(ALT) + tags + AltTag.assemble(ALT) } signer.sign(createdAt, KIND, newTags, content, onReady) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/ChannelMessageEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/ChannelMessageEvent.kt deleted file mode 100644 index 9b8d8ccb8..000000000 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/ChannelMessageEvent.kt +++ /dev/null @@ -1,115 +0,0 @@ -/** - * Copyright (c) 2024 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.quartz.nip28PublicChat - -import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohashMipMap -import com.vitorpamplona.quartz.nip10Notes.BaseTextNoteEvent -import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrl -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer -import com.vitorpamplona.quartz.nip36SensitiveContent.ContentWarningSerializer -import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetup -import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetupSerializer -import com.vitorpamplona.quartz.nip57Zaps.zapraiser.ZapRaiserSerializer -import com.vitorpamplona.quartz.nip92IMeta.IMetaTag -import com.vitorpamplona.quartz.nip92IMeta.Nip92MediaAttachments -import com.vitorpamplona.quartz.utils.TimeUtils - -@Immutable -class ChannelMessageEvent( - id: HexKey, - pubKey: HexKey, - createdAt: Long, - tags: Array>, - content: String, - sig: HexKey, -) : BaseTextNoteEvent(id, pubKey, createdAt, KIND, tags, content, sig), - IsInPublicChatChannel { - override fun channel() = - tags.firstOrNull { it.size > 3 && it[0] == "e" && it[3] == "root" }?.get(1) - ?: tags.firstOrNull { it.size > 1 && it[0] == "e" }?.get(1) - - override fun markedReplyTos() = super.markedReplyTos().filter { it != channel() } - - override fun unMarkedReplyTos() = super.unMarkedReplyTos().filter { it != channel() } - - companion object { - const val KIND = 42 - const val ALT = "Public chat message" - - fun create( - message: String, - channel: String, - replyTos: List? = null, - mentions: List? = null, - zapReceiver: List? = null, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - markAsSensitive: Boolean, - zapRaiserAmount: Long?, - directMentions: Set = emptySet(), - geohash: String? = null, - imetas: List? = null, - emojis: List? = null, - isDraft: Boolean, - onReady: (ChannelMessageEvent) -> Unit, - ) { - val tags = - mutableListOf( - arrayOf("e", channel, "", "root"), - ) - mentions?.forEach { tags.add(arrayOf("p", it)) } - replyTos?.forEach { - if (it in directMentions) { - tags.add(arrayOf("q", it)) - } else { - tags.add(arrayOf("e", it)) - } - } - zapReceiver?.forEach { tags.add(ZapSplitSetupSerializer.toTagArray(it)) } - zapRaiserAmount?.let { tags.add(ZapRaiserSerializer.toTagArray(it)) } - - if (markAsSensitive) { - tags.add(ContentWarningSerializer.toTagArray()) - } - geohash?.let { tags.addAll(geohashMipMap(it)) } - imetas?.forEach { - tags.add(Nip92MediaAttachments.createTag(it)) - } - emojis?.forEach { tags.add(it.toTagArray()) } - tags.add( - AltTagSerializer.toTagArray(ALT), - ) - - if (isDraft) { - signer.assembleRumor(createdAt, KIND, tags.toTypedArray(), message, onReady) - } else { - signer.sign(createdAt, KIND, tags.toTypedArray(), message, onReady) - } - } - } -} - -interface IsInPublicChatChannel { - fun channel(): String? -} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/ChannelMetadataEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/ChannelMetadataEvent.kt deleted file mode 100644 index 137d39136..000000000 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/ChannelMetadataEvent.kt +++ /dev/null @@ -1,100 +0,0 @@ -/** - * Copyright (c) 2024 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.quartz.nip28PublicChat - -import android.util.Log -import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey -import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer -import com.vitorpamplona.quartz.utils.TimeUtils - -@Immutable -class ChannelMetadataEvent( - id: HexKey, - pubKey: HexKey, - createdAt: Long, - tags: Array>, - content: String, - sig: HexKey, -) : Event(id, pubKey, createdAt, KIND, tags, content, sig), - IsInPublicChatChannel { - override fun channel() = tags.firstOrNull { it.size > 1 && it[0] == "e" }?.get(1) - - fun channelInfo() = - try { - EventMapper.mapper.readValue(content, ChannelCreateEvent.ChannelData::class.java) - } catch (e: Exception) { - Log.e("ChannelMetadataEvent", "Can't parse channel info $content", e) - ChannelCreateEvent.ChannelData(null, null, null) - } - - companion object { - const val KIND = 41 - const val ALT = "This is a public chat definition update" - - fun create( - name: String?, - about: String?, - picture: String?, - originalChannelIdHex: String, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (ChannelMetadataEvent) -> Unit, - ) { - create( - ChannelCreateEvent.ChannelData( - name, - about, - picture, - ), - originalChannelIdHex, - signer, - createdAt, - onReady, - ) - } - - fun create( - newChannelInfo: ChannelCreateEvent.ChannelData?, - originalChannelIdHex: String, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (ChannelMetadataEvent) -> Unit, - ) { - val content = - if (newChannelInfo != null) { - EventMapper.mapper.writeValueAsString(newChannelInfo) - } else { - "" - } - - val tags = - listOf( - arrayOf("e", originalChannelIdHex, "", "root"), - AltTagSerializer.toTagArray("Public chat update to ${newChannelInfo?.name}"), - ) - signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady) - } - } -} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip30CustomEmoji/EmojiPackSelectionEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelCreateEvent.kt similarity index 55% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip30CustomEmoji/EmojiPackSelectionEvent.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelCreateEvent.kt index 9a223590b..2779fa711 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip30CustomEmoji/EmojiPackSelectionEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelCreateEvent.kt @@ -18,47 +18,47 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip30CustomEmoji +package com.vitorpamplona.quartz.nip28PublicChat.admin import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.HexKey -import com.vitorpamplona.quartz.nip01Core.core.BaseReplaceableEvent -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip28PublicChat.base.ChannelData +import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.utils.TimeUtils @Immutable -class EmojiPackSelectionEvent( +class ChannelCreateEvent( id: HexKey, pubKey: HexKey, createdAt: Long, tags: Array>, content: String, sig: HexKey, -) : BaseReplaceableEvent(id, pubKey, createdAt, KIND, tags, content, sig) { +) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { + fun channelInfo() = ChannelData.parse(content) ?: ChannelData() + companion object { - const val KIND = 10030 - const val ALT = "Emoji selection" + const val KIND = 40 - fun createAddressATag(pubKey: HexKey) = ATag(KIND, pubKey, FIXED_D_TAG, null) - - fun createAddressTag(pubKey: HexKey) = ATag.assembleATagId(KIND, pubKey, FIXED_D_TAG) - - fun create( - listOfEmojiPacks: List?, - signer: NostrSigner, + fun build( + name: String?, + about: String?, + picture: String?, + relays: List?, createdAt: Long = TimeUtils.now(), - onReady: (EmojiPackSelectionEvent) -> Unit, - ) { - val msg = "" - val tags = mutableListOf>() + initializer: TagArrayBuilder.() -> Unit = {}, + ) = build(ChannelData(name, about, picture, relays), createdAt, initializer) - listOfEmojiPacks?.forEach { tags.add(arrayOf("a", it.toTag())) } - - tags.add(AltTagSerializer.toTagArray(ALT)) - - signer.sign(createdAt, KIND, tags.toTypedArray(), msg, onReady) + fun build( + data: ChannelData, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, data.toContent(), createdAt) { + alt("Public chat creation event ${data.name?.let { "about $it" }}") + initializer() } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/ChannelHideMessageEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelHideMessageEvent.kt similarity index 60% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/ChannelHideMessageEvent.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelHideMessageEvent.kt index 27fa3e37c..d8b2b2ad2 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/ChannelHideMessageEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelHideMessageEvent.kt @@ -18,13 +18,19 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip28PublicChat +package com.vitorpamplona.quartz.nip28PublicChat.admin import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.EventHintBundle import com.vitorpamplona.quartz.nip01Core.HexKey -import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag +import com.vitorpamplona.quartz.nip01Core.tags.events.eTags +import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEventIds +import com.vitorpamplona.quartz.nip28PublicChat.base.BasePublicChatEvent +import com.vitorpamplona.quartz.nip28PublicChat.base.channel +import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.utils.TimeUtils @Immutable @@ -35,32 +41,24 @@ class ChannelHideMessageEvent( tags: Array>, content: String, sig: HexKey, -) : Event(id, pubKey, createdAt, KIND, tags, content, sig), - IsInPublicChatChannel { - override fun channel() = - tags.firstOrNull { it.size > 3 && it[0] == "e" && it[3] == "root" }?.get(1) - ?: tags.firstOrNull { it.size > 1 && it[0] == "e" }?.get(1) - - fun eventsToHide() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } +) : BasePublicChatEvent(id, pubKey, createdAt, KIND, tags, content, sig) { + fun eventsToHide() = tags.taggedEventIds() companion object { const val KIND = 43 const val ALT = "Hide message instruction for public chats" - fun create( + fun build( reason: String, - messagesToHide: List?, - signer: NostrSigner, + messagesToHide: List, + channel: EventHintBundle, createdAt: Long = TimeUtils.now(), - onReady: (ChannelHideMessageEvent) -> Unit, - ) { - val tags = - ( - messagesToHide?.map { arrayOf("e", it) }?.toTypedArray() - ?: emptyArray() - ) + arrayOf(AltTagSerializer.toTagArray(ALT)) - - signer.sign(createdAt, KIND, tags, reason, onReady) + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, reason, createdAt) { + alt(ALT) + channel(channel) + eTags(messagesToHide) + initializer() } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelMetadataEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelMetadataEvent.kt new file mode 100644 index 000000000..a34f2d39a --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelMetadataEvent.kt @@ -0,0 +1,92 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip28PublicChat.admin + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.EventHintBundle +import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag +import com.vitorpamplona.quartz.nip28PublicChat.base.BasePublicChatEvent +import com.vitorpamplona.quartz.nip28PublicChat.base.ChannelData +import com.vitorpamplona.quartz.nip28PublicChat.base.channel +import com.vitorpamplona.quartz.nip31Alts.alt +import com.vitorpamplona.quartz.utils.TimeUtils + +@Immutable +class ChannelMetadataEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : BasePublicChatEvent(id, pubKey, createdAt, KIND, tags, content, sig) { + fun channelInfo() = ChannelData.parse(content) ?: ChannelData() + + companion object { + const val KIND = 41 + const val ALT = "This is a public chat definition update" + + fun build( + name: String?, + about: String?, + picture: String?, + relays: List?, + channel: EventHintBundle, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = build(ChannelData(name, about, picture, relays), channel, createdAt, initializer) + + fun build( + name: String?, + about: String?, + picture: String?, + relays: List?, + channel: ETag, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = build(ChannelData(name, about, picture, relays), channel, createdAt, initializer) + + fun build( + data: ChannelData, + channel: EventHintBundle, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, data.toContent(), createdAt) { + alt("Public chat update to ${data.name}") + channel(channel) + initializer() + } + + fun build( + data: ChannelData, + channel: ETag, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, data.toContent(), createdAt) { + alt("Public chat update to ${data.name}") + channel(channel) + initializer() + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/ChannelMuteUserEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelMuteUserEvent.kt similarity index 60% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/ChannelMuteUserEvent.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelMuteUserEvent.kt index d5b39ce35..66b051ed5 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/ChannelMuteUserEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelMuteUserEvent.kt @@ -18,13 +18,19 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip28PublicChat +package com.vitorpamplona.quartz.nip28PublicChat.admin import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.EventHintBundle import com.vitorpamplona.quartz.nip01Core.HexKey -import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.nip01Core.tags.people.pTags +import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUserIds +import com.vitorpamplona.quartz.nip28PublicChat.base.BasePublicChatEvent +import com.vitorpamplona.quartz.nip28PublicChat.base.channel +import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.utils.TimeUtils @Immutable @@ -35,33 +41,24 @@ class ChannelMuteUserEvent( tags: Array>, content: String, sig: HexKey, -) : Event(id, pubKey, createdAt, KIND, tags, content, sig), - IsInPublicChatChannel { - override fun channel() = - tags.firstOrNull { it.size > 3 && it[0] == "e" && it[3] == "root" }?.get(1) - ?: tags.firstOrNull { it.size > 1 && it[0] == "e" }?.get(1) - - fun usersToMute() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } +) : BasePublicChatEvent(id, pubKey, createdAt, KIND, tags, content, sig) { + fun usersToMute() = tags.taggedUserIds() companion object { const val KIND = 44 const val ALT = "Mute user instruction for public chats" - fun create( + fun build( reason: String, - usersToMute: List?, - signer: NostrSigner, + usersToMute: List, + channel: EventHintBundle, createdAt: Long = TimeUtils.now(), - onReady: (ChannelMuteUserEvent) -> Unit, - ) { - val content = reason - val tags = - ( - usersToMute?.map { arrayOf("p", it) }?.toTypedArray() - ?: emptyArray() - ) + arrayOf(AltTagSerializer.toTagArray(ALT)) - - signer.sign(createdAt, KIND, tags, content, onReady) + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, reason, createdAt) { + alt(ALT) + channel(channel) + pTags(usersToMute) + initializer() } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/base/BasePublicChatEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/base/BasePublicChatEvent.kt new file mode 100644 index 000000000..8857508ed --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/base/BasePublicChatEvent.kt @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip28PublicChat.base + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag + +@Immutable +open class BasePublicChatEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + kind: Int, + tags: Array>, + content: String, + sig: HexKey, +) : Event(id, pubKey, createdAt, kind, tags, content, sig), + IsInPublicChatChannel { + fun markedRoot() = tags.firstNotNullOfOrNull(MarkedETag::parseRoot) + + fun unmarkedRoot() = tags.firstNotNullOfOrNull(MarkedETag::parseUnmarkedRoot) + + override fun channel() = markedRoot() ?: unmarkedRoot() + + override fun channelId() = channel()?.eventId +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/base/ChannelData.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/base/ChannelData.kt new file mode 100644 index 000000000..ea1139c71 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/base/ChannelData.kt @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip28PublicChat.base + +import android.util.Log +import androidx.compose.runtime.Immutable +import com.fasterxml.jackson.module.kotlin.readValue +import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper + +@Immutable +data class ChannelData( + val name: String? = null, + val about: String? = null, + val picture: String? = null, + val relays: List? = null, +) { + fun toContent() = assemble(this) + + companion object { + fun parse(content: String): ChannelData? = + try { + EventMapper.mapper.readValue(content) + } catch (e: Exception) { + Log.e("ChannelMetadataEvent", "Can't parse channel info $content", e) + ChannelData(null, null, null, null) + } + + fun assemble(data: ChannelData) = EventMapper.mapper.writeValueAsString(data) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/base/IsInPublicChatChannel.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/base/IsInPublicChatChannel.kt new file mode 100644 index 000000000..170f3d541 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/base/IsInPublicChatChannel.kt @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip28PublicChat.base + +import com.vitorpamplona.quartz.nip01Core.core.IEvent +import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag + +interface IsInPublicChatChannel : IEvent { + fun channel(): MarkedETag? + + fun channelId(): String? +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/base/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/base/TagArrayBuilderExt.kt new file mode 100644 index 000000000..71a5e61e1 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/base/TagArrayBuilderExt.kt @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip28PublicChat.base + +import com.vitorpamplona.quartz.nip01Core.EventHintBundle +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.nip01Core.tags.people.pTag +import com.vitorpamplona.quartz.nip01Core.tags.people.pTags +import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent +import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent + +fun TagArrayBuilder.channel(rep: MarkedETag) = add(rep.toTagArray()) + +fun TagArrayBuilder.channel(rep: ETag) = add(MarkedETag.assemble(rep.eventId, rep.relay, MarkedETag.MARKER.ROOT, rep.author)) + +fun TagArrayBuilder.channel(rep: EventHintBundle) = channel(rep.toMarkedETag(MarkedETag.MARKER.ROOT)) + +fun TagArrayBuilder.reply(rep: EventHintBundle) = add(rep.toMarkedETag(MarkedETag.MARKER.REPLY).toTagArray()) + +fun TagArrayBuilder.notify(list: List) = pTags(list) + +fun TagArrayBuilder.notify(pubkey: PTag) = pTag(pubkey) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/message/ChannelMessageEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/message/ChannelMessageEvent.kt new file mode 100644 index 000000000..1d872a0e3 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/message/ChannelMessageEvent.kt @@ -0,0 +1,89 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip28PublicChat.message + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.EventHintBundle +import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag +import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent +import com.vitorpamplona.quartz.nip28PublicChat.base.IsInPublicChatChannel +import com.vitorpamplona.quartz.nip28PublicChat.base.channel +import com.vitorpamplona.quartz.nip28PublicChat.base.reply +import com.vitorpamplona.quartz.utils.TimeUtils + +@Immutable +class ChannelMessageEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : BaseThreadedEvent(id, pubKey, createdAt, KIND, tags, content, sig), + IsInPublicChatChannel { + override fun channel() = markedRoot() ?: unmarkedRoot() + + override fun channelId() = channel()?.eventId + + override fun markedReplyTos() = super.markedReplyTos().filter { it != channelId() } + + override fun unmarkedReplyTos() = super.unmarkedReplyTos().filter { it != channelId() } + + companion object { + const val KIND = 42 + const val ALT = "Public chat message" + + fun reply( + post: String, + replyingTo: EventHintBundle, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, post, createdAt) { + replyingTo.event.channel()?.let { channel(it) } + reply(replyingTo) + initializer() + } + + fun message( + post: String, + channel: EventHintBundle, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, post, createdAt) { + channel(channel) + initializer() + } + + fun message( + post: String, + channel: ETag, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, post, createdAt) { + channel(channel) + initializer() + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip30CustomEmoji/CustomEmoji.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip30CustomEmoji/CustomEmoji.kt index d8fb3323a..8cf13edce 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip30CustomEmoji/CustomEmoji.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip30CustomEmoji/CustomEmoji.kt @@ -36,7 +36,7 @@ class CustomEmoji { allTags: ImmutableListOfLists?, ): Boolean { if (allTags == null) return false - if (allTags.lists.any { it.size > 2 && it[0] == "emoji" }) return true + if (allTags.lists.any { it.size > 2 && it[0] == EmojiUrlTag.TAG_NAME }) return true return input.contains(":") } @@ -48,7 +48,7 @@ class CustomEmoji { return input.contains(":") } - fun createEmojiMap(tags: ImmutableListOfLists): Map = tags.lists.filter { it.size > 2 && it[0] == "emoji" }.associate { ":${it[1]}:" to it[2] } + fun createEmojiMap(tags: ImmutableListOfLists): Map = tags.lists.filter { it.size > 2 && it[0] == EmojiUrlTag.TAG_NAME }.associate { ":${it[1]}:" to it[2] } fun findAllEmojis(input: String): List { val matcher = customEmojiPattern.matcher(input) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip30CustomEmoji/EmojiUrlTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip30CustomEmoji/EmojiUrlTag.kt new file mode 100644 index 000000000..b3dec6802 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip30CustomEmoji/EmojiUrlTag.kt @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip30CustomEmoji + +import androidx.compose.runtime.Immutable + +@Immutable +data class EmojiUrlTag( + val code: String, + val url: String, +) { + fun encode(): String = ":$code:$url" + + fun toContentEncode(): String = ":$code" + + fun toTagArray() = arrayOf("emoji", code, url) + + companion object { + const val TAG_NAME = "emoji" + + fun decode(encodedEmojiSetup: String): EmojiUrlTag? { + val emojiParts = encodedEmojiSetup.split(":", limit = 3) + return if (emojiParts.size > 2) { + EmojiUrlTag(emojiParts[1], emojiParts[2]) + } else { + null + } + } + + fun parse(tag: Array): EmojiUrlTag? = + if (tag.size > 2 && tag[0] == "emoji") { + EmojiUrlTag(tag[1], tag[2]) + } else { + null + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip30CustomEmoji/EventExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip30CustomEmoji/EventExt.kt index bf48b06ef..dee767b07 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip30CustomEmoji/EventExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip30CustomEmoji/EventExt.kt @@ -21,6 +21,5 @@ package com.vitorpamplona.quartz.nip30CustomEmoji import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.core.mapTagged -fun Event.taggedEmojis() = tags.mapTagged("emoji") { EmojiUrl.parse(it) } +fun Event.taggedEmojis() = tags.emojis() diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip30CustomEmoji/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip30CustomEmoji/TagArrayBuilderExt.kt new file mode 100644 index 000000000..9a5067f5e --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip30CustomEmoji/TagArrayBuilderExt.kt @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip30CustomEmoji + +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder + +fun TagArrayBuilder.emoji(tag: EmojiUrlTag) = add(tag.toTagArray()) + +fun TagArrayBuilder.emojis(tags: List) = addAll(tags.map { it.toTagArray() }) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip30CustomEmoji/TagArrayExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip30CustomEmoji/TagArrayExt.kt new file mode 100644 index 000000000..fe638929f --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip30CustomEmoji/TagArrayExt.kt @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip30CustomEmoji + +import com.vitorpamplona.quartz.nip01Core.core.TagArray +import com.vitorpamplona.quartz.nip01Core.core.mapTagged + +fun TagArray.emojis() = this.mapTagged(EmojiUrlTag.TAG_NAME) { EmojiUrlTag.parse(it) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/AppRecommendationEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip30CustomEmoji/pack/EmojiPackEvent.kt similarity index 59% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/AppRecommendationEvent.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip30CustomEmoji/pack/EmojiPackEvent.kt index 1c24f0314..e0daf3ac6 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/AppRecommendationEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip30CustomEmoji/pack/EmojiPackEvent.kt @@ -18,41 +18,43 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip89AppHandlers +package com.vitorpamplona.quartz.nip30CustomEmoji.pack import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.HexKey -import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag +import com.vitorpamplona.quartz.nip31Alts.alt +import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent +import com.vitorpamplona.quartz.nip34Git.repository.name +import com.vitorpamplona.quartz.nip51Lists.GeneralListEvent import com.vitorpamplona.quartz.utils.TimeUtils +import java.util.UUID @Immutable -class AppRecommendationEvent( +class EmojiPackEvent( id: HexKey, pubKey: HexKey, createdAt: Long, tags: Array>, content: String, sig: HexKey, -) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) { - fun recommendations() = tags.filter { it.size > 1 && it[0] == "a" }.mapNotNull { ATag.parse(it[1], it.getOrNull(2)) } - +) : GeneralListEvent(id, pubKey, createdAt, KIND, tags, content, sig) { companion object { - const val KIND = 31989 - const val ALT = "App recommendations by the author" + const val KIND = 30030 + const val ALT_DESCRIPTION = "Emoji pack" - fun create( - signer: NostrSigner, + fun build( + name: String, + dTag: String = UUID.randomUUID().toString(), createdAt: Long = TimeUtils.now(), - onReady: (AppRecommendationEvent) -> Unit, - ) { - val tags = - arrayOf( - AltTagSerializer.toTagArray(ALT), - ) - signer.sign(createdAt, KIND, tags, "", onReady) + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, "", createdAt) { + alt(ALT_DESCRIPTION) + dTag(dTag) + name(name) + initializer() } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip30CustomEmoji/selection/EmojiPackSelectionEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip30CustomEmoji/selection/EmojiPackSelectionEvent.kt new file mode 100644 index 000000000..aeacfc2c2 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip30CustomEmoji/selection/EmojiPackSelectionEvent.kt @@ -0,0 +1,85 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip30CustomEmoji.selection + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.EventHintBundle +import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.BaseReplaceableEvent +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.signers.eventUpdate +import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedAddresses +import com.vitorpamplona.quartz.nip30CustomEmoji.pack.EmojiPackEvent +import com.vitorpamplona.quartz.nip31Alts.alt +import com.vitorpamplona.quartz.utils.TimeUtils + +@Immutable +class EmojiPackSelectionEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : BaseReplaceableEvent(id, pubKey, createdAt, KIND, tags, content, sig) { + fun emojiPacks() = taggedAddresses() + + companion object { + const val KIND = 10030 + const val ALT_DESCRIPTION = "Emoji selection" + + fun createAddressATag(pubKey: HexKey) = ATag(KIND, pubKey, FIXED_D_TAG, null) + + fun createAddressTag(pubKey: HexKey) = ATag.assembleATagId(KIND, pubKey, FIXED_D_TAG) + + fun add( + currentSelection: EmojiPackSelectionEvent, + packToAdd: EventHintBundle, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventUpdate(currentSelection, createdAt) { + pack(packToAdd.toATag()) + initializer() + } + + fun remove( + currentSelection: EmojiPackSelectionEvent, + packToRemove: EmojiPackEvent, + createdAt: Long = TimeUtils.now(), + updater: TagArrayBuilder.() -> Unit = {}, + ) = eventUpdate(currentSelection, createdAt) { + removePack(packToRemove.aTag(null)) + updater() + } + + fun build( + listOfEmojiPacks: List>, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, "", createdAt) { + alt(ALT_DESCRIPTION) + packs(listOfEmojiPacks.map { it.toATag() }) + initializer() + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip30CustomEmoji/selection/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip30CustomEmoji/selection/TagArrayBuilderExt.kt new file mode 100644 index 000000000..6a702f12a --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip30CustomEmoji/selection/TagArrayBuilderExt.kt @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip30CustomEmoji.selection + +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip01Core.tags.addressables.aTag +import com.vitorpamplona.quartz.nip01Core.tags.addressables.aTags +import com.vitorpamplona.quartz.nip01Core.tags.addressables.removeATag + +fun TagArrayBuilder.pack(tag: ATag) = aTag(tag) + +fun TagArrayBuilder.packs(tags: List) = aTags(tags) + +fun TagArrayBuilder.removePack(tag: ATag) = removeATag(tag) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip31Alts/AltTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip31Alts/AltTag.kt new file mode 100644 index 000000000..b28d37465 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip31Alts/AltTag.kt @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip31Alts + +class AltTag { + companion object { + const val TAG_NAME = "alt" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): String? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1] + } + + @JvmStatic + fun assemble(altDescriptor: String = "") = arrayOf(TAG_NAME, altDescriptor) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip31Alts/EventExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip31Alts/EventExt.kt index c83d28845..b23763ad4 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip31Alts/EventExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip31Alts/EventExt.kt @@ -21,8 +21,5 @@ package com.vitorpamplona.quartz.nip31Alts import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.core.firstTagValue -val ALT_TAG = "alt" - -fun Event.alt() = tags.firstTagValue(ALT_TAG) +fun Event.alt() = tags.alt() diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip31Alts/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip31Alts/TagArrayBuilderExt.kt new file mode 100644 index 000000000..87bb2b59b --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip31Alts/TagArrayBuilderExt.kt @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip31Alts + +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder + +fun TagArrayBuilder.alt(altDescriptor: String) = addUnique(AltTag.assemble(altDescriptor)) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip31Alts/AltTagSerializer.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip31Alts/TagArrayExt.kt similarity index 87% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip31Alts/AltTagSerializer.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip31Alts/TagArrayExt.kt index a696f5813..a9326360a 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip31Alts/AltTagSerializer.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip31Alts/TagArrayExt.kt @@ -20,9 +20,6 @@ */ package com.vitorpamplona.quartz.nip31Alts -class AltTagSerializer { - companion object { - @JvmStatic - fun toTagArray(altDescriptor: String = "") = arrayOf(ALT_TAG, altDescriptor) - } -} +import com.vitorpamplona.quartz.nip01Core.core.TagArray + +fun TagArray.alt() = firstNotNullOfOrNull(AltTag::parse) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/GitReplyEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/GitReplyEvent.kt deleted file mode 100644 index 74ce3b189..000000000 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/GitReplyEvent.kt +++ /dev/null @@ -1,175 +0,0 @@ -/** - * Copyright (c) 2024 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.quartz.nip34Git - -import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey -import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent -import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag -import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohashMipMap -import com.vitorpamplona.quartz.nip01Core.tags.hashtags.buildHashtagTags -import com.vitorpamplona.quartz.nip10Notes.BaseTextNoteEvent -import com.vitorpamplona.quartz.nip10Notes.content.buildUrlRefs -import com.vitorpamplona.quartz.nip10Notes.content.findHashtags -import com.vitorpamplona.quartz.nip10Notes.content.findURLs -import com.vitorpamplona.quartz.nip10Notes.positionalMarkedTags -import com.vitorpamplona.quartz.nip19Bech32.parse -import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrl -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer -import com.vitorpamplona.quartz.nip36SensitiveContent.ContentWarningSerializer -import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetup -import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetupSerializer -import com.vitorpamplona.quartz.nip57Zaps.zapraiser.ZapRaiserSerializer -import com.vitorpamplona.quartz.nip92IMeta.IMetaTag -import com.vitorpamplona.quartz.nip92IMeta.Nip92MediaAttachments -import com.vitorpamplona.quartz.utils.TimeUtils - -@Immutable -class GitReplyEvent( - id: HexKey, - pubKey: HexKey, - createdAt: Long, - tags: Array>, - content: String, - sig: HexKey, -) : BaseTextNoteEvent(id, pubKey, createdAt, KIND, tags, content, sig) { - private fun innerRepository() = - tags.firstOrNull { it.size > 3 && it[0] == "a" && it[3] == "root" } - ?: tags.firstOrNull { it.size > 1 && it[0] == "a" } - - private fun repositoryHex() = innerRepository()?.getOrNull(1) - - fun repository() = - innerRepository()?.let { - if (it.size > 1) { - val aTagValue = it[1] - val relay = it.getOrNull(2) - - ATag.parse(aTagValue, relay) - } else { - null - } - } - - fun rootIssueOrPath() = tags.lastOrNull { it.size > 3 && it[0] == "e" && it[3] == "root" }?.get(1) - - companion object { - const val KIND = 1622 - const val ALT = "A Git Reply" - - fun create( - patch: String, - createdAt: Long = TimeUtils.now(), - signer: NostrSigner, - onReady: (GitReplyEvent) -> Unit, - ) { - val content = patch - val tags = - mutableListOf( - arrayOf(), - ) - - tags.add(AltTagSerializer.toTagArray(ALT)) - - signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady) - } - - fun create( - msg: String, - replyTos: List? = null, - mentions: List? = null, - addresses: List? = null, - zapReceiver: List? = null, - markAsSensitive: Boolean = false, - zapRaiserAmount: Long? = null, - replyingTo: String? = null, - root: String? = null, - directMentions: Set = emptySet(), - geohash: String? = null, - imetas: List? = null, - emojis: List? = null, - forkedFrom: Event? = null, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - isDraft: Boolean, - onReady: (GitReplyEvent) -> Unit, - ) { - val tags = mutableListOf>() - replyTos?.let { - tags.addAll( - it.positionalMarkedTags( - tagName = "e", - root = root, - replyingTo = replyingTo, - directMentions = directMentions, - forkedFrom = forkedFrom?.id, - ), - ) - } - mentions?.forEach { - if (it in directMentions) { - tags.add(arrayOf("p", it, "", "mention")) - } else { - tags.add(arrayOf("p", it)) - } - } - replyTos?.forEach { - if (it in directMentions) { - tags.add(arrayOf("q", it)) - } - } - addresses - ?.map { it.toTag() } - ?.let { - tags.addAll( - it.positionalMarkedTags( - tagName = "a", - root = root, - replyingTo = replyingTo, - directMentions = directMentions, - forkedFrom = (forkedFrom as? AddressableEvent)?.address()?.toTag(), - ), - ) - } - tags.addAll(buildHashtagTags(findHashtags(msg))) - tags.addAll(buildUrlRefs(findURLs(msg))) - zapReceiver?.forEach { tags.add(ZapSplitSetupSerializer.toTagArray(it)) } - zapRaiserAmount?.let { tags.add(ZapRaiserSerializer.toTagArray(it)) } - if (markAsSensitive) { - tags.add(ContentWarningSerializer.toTagArray()) - } - geohash?.let { tags.addAll(geohashMipMap(it)) } - imetas?.forEach { - tags.add(Nip92MediaAttachments.createTag(it)) - } - emojis?.forEach { tags.add(it.toTagArray()) } - tags.add(AltTagSerializer.toTagArray("a git issue reply")) - - if (isDraft) { - signer.assembleRumor(createdAt, KIND, tags.toTypedArray(), msg, onReady) - } else { - signer.sign(createdAt, KIND, tags.toTypedArray(), msg, onReady) - } - } - } -} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/GitIssueEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/issue/GitIssueEvent.kt similarity index 52% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/GitIssueEvent.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/issue/GitIssueEvent.kt index 7fb1977c6..8f441aebd 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/GitIssueEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/issue/GitIssueEvent.kt @@ -18,15 +18,20 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip34Git +package com.vitorpamplona.quartz.nip34Git.issue import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.EventHintBundle import com.vitorpamplona.quartz.nip01Core.HexKey -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag -import com.vitorpamplona.quartz.nip10Notes.BaseTextNoteEvent -import com.vitorpamplona.quartz.nip19Bech32.parse -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer +import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent +import com.vitorpamplona.quartz.nip14Subject.SubjectTag +import com.vitorpamplona.quartz.nip31Alts.alt +import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent import com.vitorpamplona.quartz.utils.TimeUtils @Immutable @@ -37,46 +42,35 @@ class GitIssueEvent( tags: Array>, content: String, sig: HexKey, -) : BaseTextNoteEvent(id, pubKey, createdAt, KIND, tags, content, sig) { - private fun innerRepository() = - tags.firstOrNull { it.size > 3 && it[0] == "a" && it[3] == "root" } - ?: tags.firstOrNull { it.size > 1 && it[0] == "a" } +) : BaseThreadedEvent(id, pubKey, createdAt, KIND, tags, content, sig) { + fun repositoryHex() = tags.firstNotNullOfOrNull(ATag::parseAddress) - private fun repositoryHex() = innerRepository()?.getOrNull(1) + fun repository() = tags.firstNotNullOfOrNull(ATag::parse) - fun rootIssueOrPatch() = tags.lastOrNull { it.size > 3 && it[0] == "e" && it[3] == "root" }?.get(1) + fun topics() = hashtags() - fun repository() = - innerRepository()?.let { - if (it.size > 1) { - val aTagValue = it[1] - val relay = it.getOrNull(2) - - ATag.parse(aTagValue, relay) - } else { - null - } - } + fun subject() = tags.firstNotNullOfOrNull(SubjectTag::parse) companion object { const val KIND = 1621 - const val ALT = "A Git Issue" + const val ALT_DESCRIPTION = "A Git Issue" - fun create( - patch: String, + fun build( + subject: String, + content: String, + repository: EventHintBundle, + notify: List, + topics: List, createdAt: Long = TimeUtils.now(), - signer: NostrSigner, - onReady: (GitIssueEvent) -> Unit, - ) { - val content = patch - val tags = - mutableListOf( - arrayOf(), - ) + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, content, createdAt) { + alt(ALT_DESCRIPTION) + subject(subject) + repository(repository) + notify(notify) + hashtags(topics) - tags.add(AltTagSerializer.toTagArray(ALT)) - - signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady) + initializer() } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/issue/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/issue/TagArrayBuilderExt.kt new file mode 100644 index 000000000..7a99b82fd --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/issue/TagArrayBuilderExt.kt @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip34Git.issue + +import com.vitorpamplona.quartz.nip01Core.EventHintBundle +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.nip01Core.tags.people.pTags +import com.vitorpamplona.quartz.nip14Subject.SubjectTag +import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent + +fun TagArrayBuilder.repository(rep: ATag) = addUnique(rep.toATagArray()) + +fun TagArrayBuilder.repository(rep: EventHintBundle) = addUnique(rep.toATag().toATagArray()) + +fun TagArrayBuilder.notify(list: List) = pTags(list) + +fun TagArrayBuilder.subject(subject: String) = add(SubjectTag.assemble(subject)) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/GitPatchEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/patch/GitPatchEvent.kt similarity index 94% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/GitPatchEvent.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/patch/GitPatchEvent.kt index befa100b7..5f7a6cbf3 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/GitPatchEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/patch/GitPatchEvent.kt @@ -18,15 +18,14 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip34Git +package com.vitorpamplona.quartz.nip34Git.patch import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag -import com.vitorpamplona.quartz.nip19Bech32.parse -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer +import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.utils.TimeUtils @Immutable @@ -90,7 +89,7 @@ class GitPatchEvent( arrayOf(), ) - tags.add(AltTagSerializer.toTagArray(ALT)) + tags.add(AltTag.assemble(ALT)) signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/reply/GitReplyEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/reply/GitReplyEvent.kt new file mode 100644 index 000000000..d8f6e1ba1 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/reply/GitReplyEvent.kt @@ -0,0 +1,93 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip34Git.reply + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.EventHintBundle +import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent +import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag +import com.vitorpamplona.quartz.nip10Notes.tags.markedETags +import com.vitorpamplona.quartz.nip10Notes.tags.prepareMarkedETagsAsReplyTo +import com.vitorpamplona.quartz.nip34Git.issue.GitIssueEvent +import com.vitorpamplona.quartz.nip34Git.patch.GitPatchEvent +import com.vitorpamplona.quartz.utils.TimeUtils +import com.vitorpamplona.quartz.utils.lastNotNullOfOrNull + +@Immutable +class GitReplyEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : BaseThreadedEvent(id, pubKey, createdAt, KIND, tags, content, sig) { + fun repositoryHex() = tags.firstNotNullOfOrNull(ATag::parseAddress) + + fun repository() = tags.firstNotNullOfOrNull(ATag::parse) + + fun rootIssueOrPatch() = tags.lastNotNullOfOrNull(MarkedETag::parseRootId) + + companion object { + const val KIND = 1622 + const val ALT_DESCRIPTION = "A Git Reply" + + fun reply( + post: String, + replyingTo: EventHintBundle, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, post, createdAt) { + replyingTo.event.repository()?.let { repository(it) } + markedETags(prepareMarkedETagsAsReplyTo(replyingTo)) + + initializer() + } + + fun replyIssue( + post: String, + issue: EventHintBundle, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, post, createdAt) { + issue.event.repository()?.let { repository(it) } + issue(issue) + + initializer() + } + + fun replyPatch( + post: String, + patch: EventHintBundle, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, post, createdAt) { + patch.event.repository()?.let { repository(it) } + patch(patch) + + initializer() + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/reply/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/reply/TagArrayBuilderExt.kt new file mode 100644 index 000000000..a3634c410 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/reply/TagArrayBuilderExt.kt @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip34Git.reply + +import com.vitorpamplona.quartz.nip01Core.EventHintBundle +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.nip01Core.tags.people.pTags +import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag +import com.vitorpamplona.quartz.nip34Git.issue.GitIssueEvent +import com.vitorpamplona.quartz.nip34Git.patch.GitPatchEvent +import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent + +fun TagArrayBuilder.repository(rep: ATag) = addUnique(rep.toATagArray()) + +fun TagArrayBuilder.repository(rep: EventHintBundle) = addUnique(rep.toATag().toATagArray()) + +fun TagArrayBuilder.patch(rep: EventHintBundle) = addUnique(rep.toMarkedETag(MarkedETag.MARKER.ROOT).toTagArray()) + +fun TagArrayBuilder.issue(rep: EventHintBundle) = addUnique(rep.toMarkedETag(MarkedETag.MARKER.ROOT).toTagArray()) + +fun TagArrayBuilder.notify(list: List) = pTags(list) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/GitRepositoryEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/repository/GitRepositoryEvent.kt similarity index 53% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/GitRepositoryEvent.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/repository/GitRepositoryEvent.kt index 090121e35..857b95be0 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/GitRepositoryEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/repository/GitRepositoryEvent.kt @@ -18,14 +18,22 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip34Git +package com.vitorpamplona.quartz.nip34Git.repository import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag +import com.vitorpamplona.quartz.nip31Alts.alt +import com.vitorpamplona.quartz.nip34Git.reply.GitReplyEvent.Companion.ALT_DESCRIPTION +import com.vitorpamplona.quartz.nip34Git.repository.tags.CloneTag +import com.vitorpamplona.quartz.nip34Git.repository.tags.DescriptionTag +import com.vitorpamplona.quartz.nip34Git.repository.tags.NameTag +import com.vitorpamplona.quartz.nip34Git.repository.tags.WebTag import com.vitorpamplona.quartz.utils.TimeUtils +import java.util.UUID @Immutable class GitRepositoryEvent( @@ -36,26 +44,34 @@ class GitRepositoryEvent( content: String, sig: HexKey, ) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) { - fun name() = tags.firstOrNull { it.size > 1 && it[0] == "name" }?.get(1) + fun name() = tags.firstNotNullOfOrNull(NameTag::parse) - fun description() = tags.firstOrNull { it.size > 1 && it[0] == "description" }?.get(1) + fun description() = tags.firstNotNullOfOrNull(DescriptionTag::parse) - fun web() = tags.firstOrNull { it.size > 1 && it[0] == "web" }?.get(1) + fun web() = tags.firstNotNullOfOrNull(WebTag::parse) - fun clone() = tags.firstOrNull { it.size > 1 && it[0] == "clone" }?.get(1) + fun clone() = tags.firstNotNullOfOrNull(CloneTag::parse) companion object { const val KIND = 30617 const val ALT = "Git Repository" - fun create( - signer: NostrSigner, + fun build( + name: String, + description: String? = null, + webUrl: String? = null, + cloneUrl: String? = null, + dTag: String = UUID.randomUUID().toString(), createdAt: Long = TimeUtils.now(), - onReady: (GitRepositoryEvent) -> Unit, - ) { - val tags = mutableListOf>() - tags.add(AltTagSerializer.toTagArray(ALT)) - signer.sign(createdAt, KIND, tags.toTypedArray(), "", onReady) + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, "", createdAt) { + alt(ALT_DESCRIPTION) + dTag(dTag) + name(name) + description?.let { description(it) } + webUrl?.let { webUrl(it) } + cloneUrl?.let { cloneUrl(it) } + initializer() } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/repository/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/repository/TagArrayBuilderExt.kt new file mode 100644 index 000000000..ffd653f6b --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/repository/TagArrayBuilderExt.kt @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip34Git.repository + +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip34Git.repository.tags.CloneTag +import com.vitorpamplona.quartz.nip34Git.repository.tags.DescriptionTag +import com.vitorpamplona.quartz.nip34Git.repository.tags.NameTag +import com.vitorpamplona.quartz.nip34Git.repository.tags.WebTag + +fun TagArrayBuilder.name(name: String) = addUnique(NameTag.assemble(name)) + +fun TagArrayBuilder.description(description: String) = addUnique(DescriptionTag.assemble(description)) + +fun TagArrayBuilder.webUrl(webUrl: String) = addUnique(WebTag.assemble(webUrl)) + +fun TagArrayBuilder.cloneUrl(cloneUrl: String) = addUnique(CloneTag.assemble(cloneUrl)) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/repository/tags/CloneTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/repository/tags/CloneTag.kt new file mode 100644 index 000000000..47b1e1f18 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/repository/tags/CloneTag.kt @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip34Git.repository.tags + +class CloneTag { + companion object { + const val TAG_NAME = "clone" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): String? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1] + } + + @JvmStatic + fun assemble(name: String) = arrayOf(TAG_NAME, name) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/repository/tags/DescriptionTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/repository/tags/DescriptionTag.kt new file mode 100644 index 000000000..4256d7efe --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/repository/tags/DescriptionTag.kt @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip34Git.repository.tags + +class DescriptionTag { + companion object { + const val TAG_NAME = "description" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): String? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1] + } + + @JvmStatic + fun assemble(name: String) = arrayOf(TAG_NAME, name) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/repository/tags/NameTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/repository/tags/NameTag.kt new file mode 100644 index 000000000..8fc6aff04 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/repository/tags/NameTag.kt @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip34Git.repository.tags + +class NameTag { + companion object { + const val TAG_NAME = "name" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): String? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1] + } + + @JvmStatic + fun assemble(name: String) = arrayOf(TAG_NAME, name) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/repository/tags/WebTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/repository/tags/WebTag.kt new file mode 100644 index 000000000..5c5445856 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/repository/tags/WebTag.kt @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip34Git.repository.tags + +class WebTag { + companion object { + const val TAG_NAME = "web" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): String? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1] + } + + @JvmStatic + fun assemble(name: String) = arrayOf(TAG_NAME, name) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip35Torrents/DefaultTrackers.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip35Torrents/DefaultTrackers.kt new file mode 100644 index 000000000..81fb74c6a --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip35Torrents/DefaultTrackers.kt @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip35Torrents + +val DEFAULT_TRACKERS = + listOf( + "http://tracker.loadpeers.org:8080/xvRKfvAlnfuf5EfxTT5T0KIVPtbqAHnX/announce", + "udp://tracker.coppersurfer.tk:6969/announce", + "udp://tracker.openbittorrent.com:6969/announce", + "udp://open.stealth.si:80/announce", + "udp://tracker.torrent.eu.org:451/announce", + "udp://tracker.opentrackr.org:1337", + ) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip35Torrents/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip35Torrents/TagArrayBuilderExt.kt new file mode 100644 index 000000000..bca45954c --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip35Torrents/TagArrayBuilderExt.kt @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip35Torrents + +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip23LongContent.tags.TitleTag +import com.vitorpamplona.quartz.nip35Torrents.tags.BtihTag +import com.vitorpamplona.quartz.nip35Torrents.tags.FileTag +import com.vitorpamplona.quartz.nip35Torrents.tags.InfoHashTag +import com.vitorpamplona.quartz.nip35Torrents.tags.TrackerTag + +fun TagArrayBuilder.title(title: String) = addUnique(TitleTag.assemble(title)) + +fun TagArrayBuilder.btih(btih: String) = addUnique(BtihTag.assemble(btih)) + +fun TagArrayBuilder.infohash(hash: String) = addUnique(InfoHashTag.assemble(hash)) + +fun TagArrayBuilder.file(tag: FileTag) = add(tag.toTagArray()) + +fun TagArrayBuilder.files(tags: List) = addAll(tags.map { it.toTagArray() }) + +fun TagArrayBuilder.tracker(uri: String) = add(TrackerTag.assemble(uri)) + +fun TagArrayBuilder.trackers(uris: List) = addAll(uris.map { TrackerTag.assemble(it) }) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip35Torrents/TorrentCommentEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip35Torrents/TorrentCommentEvent.kt index 9c022b150..fad34983c 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip35Torrents/TorrentCommentEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip35Torrents/TorrentCommentEvent.kt @@ -21,26 +21,18 @@ package com.vitorpamplona.quartz.nip35Torrents import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.EventHintBundle import com.vitorpamplona.quartz.nip01Core.HexKey -import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent -import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag -import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohashMipMap -import com.vitorpamplona.quartz.nip01Core.tags.hashtags.buildHashtagTags -import com.vitorpamplona.quartz.nip10Notes.BaseTextNoteEvent -import com.vitorpamplona.quartz.nip10Notes.content.buildUrlRefs -import com.vitorpamplona.quartz.nip10Notes.content.findHashtags -import com.vitorpamplona.quartz.nip10Notes.content.findURLs -import com.vitorpamplona.quartz.nip10Notes.positionalMarkedTags -import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrl -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer -import com.vitorpamplona.quartz.nip36SensitiveContent.ContentWarningSerializer -import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetup -import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetupSerializer -import com.vitorpamplona.quartz.nip57Zaps.zapraiser.ZapRaiserSerializer -import com.vitorpamplona.quartz.nip92IMeta.IMetaTag -import com.vitorpamplona.quartz.nip92IMeta.Nip92MediaAttachments +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag +import com.vitorpamplona.quartz.nip01Core.tags.events.eTags +import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEvents +import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent +import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag +import com.vitorpamplona.quartz.nip10Notes.tags.positionalMarkedTags +import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.utils.TimeUtils @Immutable @@ -51,97 +43,48 @@ class TorrentCommentEvent( tags: Array>, content: String, sig: HexKey, -) : BaseTextNoteEvent(id, pubKey, createdAt, KIND, tags, content, sig) { - private fun innerTorrent() = - tags.firstOrNull { it.size > 3 && it[0] == "e" && it[3] == "root" } - ?: tags.firstOrNull { it.size > 1 && it[0] == "e" } +) : BaseThreadedEvent(id, pubKey, createdAt, KIND, tags, content, sig) { + fun torrent() = tags.firstNotNullOfOrNull(MarkedETag::parseRoot) ?: tags.firstNotNullOfOrNull(ETag::parse) - fun torrent() = innerTorrent()?.getOrNull(1) + fun torrentIds() = tags.firstNotNullOfOrNull(MarkedETag::parseRootId) ?: tags.firstNotNullOfOrNull(ETag::parseId) companion object { const val KIND = 2004 - const val ALT = "Comment for a Torrent file" + const val ALT_DESCRIPTION = "Comment for a Torrent file" - fun create( + fun build( message: String, - torrent: HexKey, - replyTos: List? = null, - mentions: List? = null, - addresses: List? = null, - zapReceiver: List? = null, - signer: NostrSigner, + torrent: EventHintBundle, + replyingTo: EventHintBundle?, createdAt: Long = TimeUtils.now(), - markAsSensitive: Boolean, - replyingTo: String? = null, - directMentions: Set = emptySet(), - zapRaiserAmount: Long?, - geohash: String? = null, - imetas: List? = null, - emojis: List? = null, - forkedFrom: Event? = null, - isDraft: Boolean, - onReady: (TorrentCommentEvent) -> Unit, - ) { - val content = message - - val tags = mutableListOf>() - replyTos?.let { - tags.addAll( - it.positionalMarkedTags( - tagName = "e", - root = torrent, - replyingTo = replyingTo, - directMentions = directMentions, - forkedFrom = forkedFrom?.id, - ), - ) - } - mentions?.forEach { - if (it in directMentions) { - tags.add(arrayOf("p", it, "", "mention")) + ): EventTemplate { + val eTags = + if (replyingTo == null) { + listOfNotNull(torrent.toETag()) } else { - tags.add(arrayOf("p", it)) + replyingTo.event.taggedEvents() + replyingTo.toETag() } - } - replyTos?.forEach { - if (it in directMentions) { - tags.add(arrayOf("q", it)) - } - } - addresses - ?.map { it.toTag() } - ?.let { - tags.addAll( - it.positionalMarkedTags( - tagName = "a", - root = torrent, - replyingTo = replyingTo, - directMentions = directMentions, - forkedFrom = (forkedFrom as? AddressableEvent)?.address()?.toTag(), - ), - ) - } - tags.addAll(buildHashtagTags(findHashtags(message))) - tags.addAll(buildUrlRefs(findURLs(message))) - zapReceiver?.forEach { tags.add(ZapSplitSetupSerializer.toTagArray(it)) } - zapRaiserAmount?.let { tags.add(ZapRaiserSerializer.toTagArray(it)) } + // double check the order and erases older markers. + val sortedAndMarked = + eTags.positionalMarkedTags( + root = torrent.toETag(), + replyingTo = replyingTo?.toETag(), + forkedFrom = null, + ) - if (markAsSensitive) { - tags.add(ContentWarningSerializer.toTagArray()) + return build(message, createdAt) { + eTags(sortedAndMarked) } - geohash?.let { tags.addAll(geohashMipMap(it)) } - imetas?.forEach { - tags.add(Nip92MediaAttachments.createTag(it)) - } - emojis?.forEach { tags.add(it.toTagArray()) } - tags.add(AltTagSerializer.toTagArray(ALT)) + } - if (isDraft) { - signer.assembleRumor(createdAt, KIND, tags.toTypedArray(), content, onReady) - } else { - signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady) - } + fun build( + post: String, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, post, createdAt) { + alt(ALT_DESCRIPTION) + initializer() } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip35Torrents/TorrentEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip35Torrents/TorrentEvent.kt index 5b201d6b9..cf5c42caa 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip35Torrents/TorrentEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip35Torrents/TorrentEvent.kt @@ -24,11 +24,21 @@ import android.net.Uri import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.core.firstTagValue -import com.vitorpamplona.quartz.nip01Core.core.mapValues -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer -import com.vitorpamplona.quartz.nip36SensitiveContent.ContentWarningSerializer +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags +import com.vitorpamplona.quartz.nip01Core.tags.references.references +import com.vitorpamplona.quartz.nip10Notes.content.findHashtags +import com.vitorpamplona.quartz.nip10Notes.content.findNostrUris +import com.vitorpamplona.quartz.nip10Notes.content.findURLs +import com.vitorpamplona.quartz.nip18Reposts.quotes.quotes +import com.vitorpamplona.quartz.nip23LongContent.tags.TitleTag +import com.vitorpamplona.quartz.nip31Alts.alt +import com.vitorpamplona.quartz.nip35Torrents.tags.BtihTag +import com.vitorpamplona.quartz.nip35Torrents.tags.FileTag +import com.vitorpamplona.quartz.nip35Torrents.tags.InfoHashTag +import com.vitorpamplona.quartz.nip35Torrents.tags.TrackerTag +import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning import com.vitorpamplona.quartz.utils.TimeUtils @Immutable @@ -40,15 +50,15 @@ class TorrentEvent( content: String, sig: HexKey, ) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { - fun title() = tags.firstTagValue("title") + fun title() = tags.firstNotNullOfOrNull(TitleTag::parse) - fun btih() = tags.firstTagValue("btih") + fun btih() = tags.firstNotNullOfOrNull(BtihTag::parse) - fun x() = tags.firstTagValue("x") + fun x() = tags.firstNotNullOfOrNull(InfoHashTag::parse) - fun trackers() = tags.mapValues("tracker") + fun trackers() = tags.mapNotNull(TrackerTag::parse) - fun files() = tags.filter { it.size > 1 && it[0] == "file" }.map { TorrentFile(it[1], it.getOrNull(2)?.toLongOrNull()) } + fun files() = tags.mapNotNull(FileTag::parse) fun toMagnetLink(): String { val builder = Uri.Builder() @@ -64,64 +74,45 @@ class TorrentEvent( return builder.build().toString() } - fun totalSizeBytes(): Long = tags.filter { it.size > 1 && it[0] == "file" }.sumOf { it.getOrNull(2)?.toLongOrNull() ?: 0L } + fun totalSizeBytes(): Long = tags.sumOf { FileTag.parseBytes(it) ?: 0L } companion object { const val KIND = 2003 const val ALT_DESCRIPTION = "A torrent file" - val DEFAULT_TRACKERS = - listOf( - "http://tracker.loadpeers.org:8080/xvRKfvAlnfuf5EfxTT5T0KIVPtbqAHnX/announce", - "udp://tracker.coppersurfer.tk:6969/announce", - "udp://tracker.openbittorrent.com:6969/announce", - "udp://open.stealth.si:80/announce", - "udp://tracker.torrent.eu.org:451/announce", - "udp://tracker.opentrackr.org:1337", - ) + fun build( + description: String?, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, description ?: "", createdAt) { + alt(ALT_DESCRIPTION) + initializer() + } - fun create( + fun build( title: String, btih: String, - files: List, + files: List, description: String? = null, x: String? = null, trackers: List? = null, alt: String? = null, - sensitiveContent: Boolean? = null, - signer: NostrSigner, + contentWarningReason: String? = null, createdAt: Long = TimeUtils.now(), - onReady: (TorrentEvent) -> Unit, - ) { - val tags = - listOfNotNull( - arrayOf("title", title), - arrayOf("btih", btih), - x?.let { arrayOf("x", it) }, - alt?.let { arrayOf("alt", it) } ?: AltTagSerializer.toTagArray(ALT_DESCRIPTION), - sensitiveContent?.let { - if (it) { - ContentWarningSerializer.toTagArray() - } else { - null - } - }, - ) + - files.map { - if (it.bytes != null) { - arrayOf(it.fileName, it.bytes.toString()) - } else { - arrayOf(it.fileName) - } - } + - ( - trackers?.map { - arrayOf(it) - } ?: emptyList() - ) + ) = eventTemplate(KIND, description ?: "", createdAt) { + alt(alt ?: ALT_DESCRIPTION) + title(title) + btih(btih) + files(files) + trackers?.let { trackers(it) } + x?.let { infohash(it) } + contentWarningReason?.let { contentWarning(it) } - val content = description ?: "" - signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady) + description?.let { + hashtags(findHashtags(it)) + references(findURLs(it)) + quotes(findNostrUris(it)) + } } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip35Torrents/tags/BtihTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip35Torrents/tags/BtihTag.kt new file mode 100644 index 000000000..85697ae11 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip35Torrents/tags/BtihTag.kt @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip35Torrents.tags + +class BtihTag { + companion object { + const val TAG_NAME = "btih" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): String? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1] + } + + @JvmStatic + fun assemble(btih: String) = arrayOf(TAG_NAME, btih) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip35Torrents/tags/FileTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip35Torrents/tags/FileTag.kt new file mode 100644 index 000000000..895eb423c --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip35Torrents/tags/FileTag.kt @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip35Torrents.tags + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip46RemoteSigner.getOrNull + +@Immutable +class FileTag( + val fileName: String, + val bytes: Long?, +) { + fun toTagArray() = assemble(fileName, bytes) + + companion object { + const val TAG_NAME = "file" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): FileTag? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return FileTag(tag[1], tag.getOrNull(2)?.toLongOrNull()) + } + + @JvmStatic + fun parseBytes(tag: Array): Long? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag.getOrNull(2)?.toLongOrNull() + } + + @JvmStatic + fun assemble( + name: String, + bytes: Long?, + ) = arrayOf(TAG_NAME, name, bytes.toString()) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip35Torrents/tags/InfoHashTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip35Torrents/tags/InfoHashTag.kt new file mode 100644 index 000000000..744aad94d --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip35Torrents/tags/InfoHashTag.kt @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip35Torrents.tags + +class InfoHashTag { + companion object { + const val TAG_NAME = "x" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): String? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1] + } + + @JvmStatic + fun assemble(hash: String) = arrayOf(TAG_NAME, hash) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip35Torrents/tags/TrackerTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip35Torrents/tags/TrackerTag.kt new file mode 100644 index 000000000..8282c3670 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip35Torrents/tags/TrackerTag.kt @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip35Torrents.tags + +class TrackerTag { + companion object { + const val TAG_NAME = "tracker" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): String? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1] + } + + @JvmStatic + fun assemble(uri: String) = arrayOf(TAG_NAME, uri) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip36SensitiveContent/ContentWarningTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip36SensitiveContent/ContentWarningTag.kt new file mode 100644 index 000000000..593bad3a7 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip36SensitiveContent/ContentWarningTag.kt @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip36SensitiveContent + +import com.vitorpamplona.quartz.utils.bytesUsedInMemory +import com.vitorpamplona.quartz.utils.pointerSizeInBytes + +class ContentWarningTag( + val reason: String, +) { + fun countMemory(): Long = 1 * pointerSizeInBytes + reason.bytesUsedInMemory() + + fun toTagArray() = assemble(reason) + + companion object { + const val TAG_NAME = "content-warning" + + @JvmStatic + fun parse(tags: Array): ContentWarningTag { + require(tags[0] == TAG_NAME) + return ContentWarningTag(tags[1]) + } + + @JvmStatic + fun assemble(reason: String) = arrayOf(TAG_NAME, reason) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip36SensitiveContent/EventExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip36SensitiveContent/EventExt.kt index 1568449dc..1262f7e76 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip36SensitiveContent/EventExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip36SensitiveContent/EventExt.kt @@ -22,12 +22,6 @@ package com.vitorpamplona.quartz.nip36SensitiveContent import com.vitorpamplona.quartz.nip01Core.core.Event -const val CONTENT_WARNING = "content-warning" +fun Event.isSensitive() = tags.isSensitive() -fun Event.isSensitive() = tags.any { (it.size > 0 && it[0] == CONTENT_WARNING) } - -fun Event.isSensitiveOrNSFW() = - tags.any { - (it.size > 0 && it[0] == CONTENT_WARNING) || - (it.size > 1 && it[0] == "t" && (it[1].equals("nsfw", true) || it[1].equals("nude", true))) - } +fun Event.isSensitiveOrNSFW() = tags.isSensitiveOrNSFW() diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip36SensitiveContent/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip36SensitiveContent/TagArrayBuilderExt.kt new file mode 100644 index 000000000..184b8b6bd --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip36SensitiveContent/TagArrayBuilderExt.kt @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip36SensitiveContent + +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder + +fun TagArrayBuilder.contentWarning(reason: String) = add(ContentWarningTag.assemble(reason)) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip36SensitiveContent/TagArrayExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip36SensitiveContent/TagArrayExt.kt new file mode 100644 index 000000000..dc804d02e --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip36SensitiveContent/TagArrayExt.kt @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip36SensitiveContent + +import com.vitorpamplona.quartz.nip01Core.core.TagArray + +fun TagArray.isSensitive() = this.any { (it.size > 0 && it[0] == ContentWarningTag.TAG_NAME) } + +fun TagArray.isSensitiveOrNSFW() = + this.any { + (it.size > 0 && it[0] == ContentWarningTag.TAG_NAME) || + (it.size > 1 && it[0] == "t" && (it[1].equals("nsfw", true) || it[1].equals("nude", true))) + } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip37Drafts/DraftEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip37Drafts/DraftEvent.kt index a3576c6f8..c75bc3140 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip37Drafts/DraftEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip37Drafts/DraftEvent.kt @@ -30,10 +30,10 @@ import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent -import com.vitorpamplona.quartz.nip28PublicChat.ChannelMessageEvent -import com.vitorpamplona.quartz.nip34Git.GitReplyEvent +import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent +import com.vitorpamplona.quartz.nip34Git.reply.GitReplyEvent import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent -import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesChatMessageEvent +import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.pointerSizeInBytes @@ -175,7 +175,7 @@ class DraftEvent( onReady: (DraftEvent) -> Unit, ) { val tags = mutableListOf>() - originalNote.channel()?.let { tags.add(arrayOf("e", it)) } + originalNote.channelId()?.let { tags.add(arrayOf("e", it)) } create(dTag, originalNote, tags, signer, createdAt, onReady) } @@ -216,7 +216,7 @@ class DraftEvent( createdAt: Long = TimeUtils.now(), onReady: (DraftEvent) -> Unit, ) { - val tagsWithMarkers = originalNote.getRootScopes() + originalNote.getDirectReplies() + val tagsWithMarkers = originalNote.rootScopes() + originalNote.directReplies() create(dTag, originalNote, tagsWithMarkers, signer, createdAt, onReady) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip39ExtIdentities/EventExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip39ExtIdentities/EventExt.kt index 522659624..0bda989a4 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip39ExtIdentities/EventExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip39ExtIdentities/EventExt.kt @@ -21,9 +21,9 @@ package com.vitorpamplona.quartz.nip39ExtIdentities import android.util.Log -import com.vitorpamplona.quartz.nip01Core.MetadataEvent import com.vitorpamplona.quartz.nip01Core.core.TagArray import com.vitorpamplona.quartz.nip01Core.core.mapTagged +import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent fun MetadataEvent.identityClaims() = tags.mapTagged("i") { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip44Encryption/Nip44.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip44Encryption/Nip44.kt index c7018bac6..03bb5fff3 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip44Encryption/Nip44.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip44Encryption/Nip44.kt @@ -20,8 +20,10 @@ */ package com.vitorpamplona.quartz.nip44Encryption +import android.util.Log import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper -import com.vitorpamplona.quartz.nip04Dm.Nip04 +import com.vitorpamplona.quartz.nip04Dm.Nip04Encoder +import com.vitorpamplona.quartz.nip04Dm.Nip04Encryption import fr.acinq.secp256k1.Secp256k1 import java.security.SecureRandom import java.util.Base64 @@ -29,7 +31,7 @@ import java.util.Base64 class Nip44( secp256k1: Secp256k1, random: SecureRandom, - val nip04: Nip04, + val nip04: Nip04Encryption, ) { public val v1 = Nip44v1(secp256k1, random) public val v2 = Nip44v2(secp256k1, random) @@ -85,12 +87,19 @@ class Nip44( privateKey: ByteArray, pubKey: ByteArray, ): String? { - val info = EventMapper.mapper.readValue(json, EncryptedInfoString::class.java) + // Ignores if it is not a valid json + val info = + try { + EventMapper.mapper.readValue(json, EncryptedInfoString::class.java) + } catch (e: Exception) { + Log.e("NIP44", "Unable to parse json $json") + return null + } return when (info.v) { - Nip04.EncryptedInfo.V -> { + Nip04Encoder.V -> { val encryptedInfo = - Nip04.EncryptedInfo( + Nip04Encoder( ciphertext = Base64.getDecoder().decode(info.ciphertext), nonce = Base64.getDecoder().decode(info.nonce), ) @@ -127,10 +136,17 @@ class Nip44( ): String? { if (payload.isEmpty()) return null - val byteArray = Base64.getDecoder().decode(payload) + // Ignores if it is not base64 + val byteArray = + try { + Base64.getDecoder().decode(payload) + } catch (e: Exception) { + Log.e("NIP44", "Unable to parse base64 $payload") + return null + } return when (byteArray[0].toInt()) { - Nip04.EncryptedInfo.V -> nip04.decrypt(payload, privateKey, pubKey) + Nip04Encoder.V -> nip04.decrypt(payload, privateKey, pubKey) Nip44v1.EncryptedInfo.V -> v1.decrypt(payload, privateKey, pubKey) Nip44v2.EncryptedInfo.V -> v2.decrypt(payload, privateKey, pubKey) else -> null diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip46RemoteSigner/BunkerResponseGetRelays.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip46RemoteSigner/BunkerResponseGetRelays.kt index 50a507b11..eff4ce118 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip46RemoteSigner/BunkerResponseGetRelays.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip46RemoteSigner/BunkerResponseGetRelays.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.quartz.nip46RemoteSigner import com.fasterxml.jackson.core.type.TypeReference import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper -import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent.ReadWrite +import com.vitorpamplona.quartz.nip02FollowList.ReadWrite import java.util.UUID class BunkerResponseGetRelays( diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip46RemoteSigner/NostrConnectEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip46RemoteSigner/NostrConnectEvent.kt index e9d32c51f..bf4b55b54 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip46RemoteSigner/NostrConnectEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip46RemoteSigner/NostrConnectEvent.kt @@ -25,7 +25,7 @@ import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer +import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.utils.Hex import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.pointerSizeInBytes @@ -94,7 +94,7 @@ class NostrConnectEvent( ) { val tags = arrayOf( - AltTagSerializer.toTagArray(ALT), + AltTag.assemble(ALT), arrayOf("p", remoteKey), ) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentRequestEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentRequestEvent.kt index 97850dd33..854250f2e 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentRequestEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentRequestEvent.kt @@ -26,7 +26,7 @@ import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer +import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.pointerSizeInBytes @@ -86,7 +86,7 @@ class LnZapPaymentRequestEvent( ) { val serializedRequest = EventMapper.mapper.writeValueAsString(PayInvoiceMethod.create(lnInvoice)) - val tags = arrayOf(arrayOf("p", walletServicePubkey), AltTagSerializer.toTagArray(ALT)) + val tags = arrayOf(arrayOf("p", walletServicePubkey), AltTag.assemble(ALT)) signer.nip04Encrypt( serializedRequest, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip48ProxyTags/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip48ProxyTags/TagArrayBuilderExt.kt index 4c7af5e9a..3c21b3489 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip48ProxyTags/TagArrayBuilderExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip48ProxyTags/TagArrayBuilderExt.kt @@ -20,16 +20,17 @@ */ package com.vitorpamplona.quartz.nip48ProxyTags +import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder -fun TagArrayBuilder.proxy( +fun TagArrayBuilder.proxy( id: String, pt: ProxyTag.Protocol, ) = add(ProxyTag.assemble(id, pt.code)) -fun TagArrayBuilder.proxy( +fun TagArrayBuilder.proxy( id: String, pt: String, ) = add(ProxyTag.assemble(id, pt)) -fun TagArrayBuilder.proxy(tag: ProxyTag) = add(tag.toTagArray()) +fun TagArrayBuilder.proxy(tag: ProxyTag) = add(tag.toTagArray()) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip50Search/SearchRelayListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip50Search/SearchRelayListEvent.kt index c84f395f3..1e17ada6a 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip50Search/SearchRelayListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip50Search/SearchRelayListEvent.kt @@ -26,7 +26,7 @@ import com.vitorpamplona.quartz.nip01Core.core.BaseReplaceableEvent import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer +import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.utils.TimeUtils @Immutable @@ -58,7 +58,7 @@ class SearchRelayListEvent( relays .map { arrayOf("relay", it) - }.plusElement(AltTagSerializer.toTagArray("Relay list to use for Search")) + }.plusElement(AltTag.assemble("Relay list to use for Search")) .toTypedArray() fun updateRelayList( diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/BookmarkListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/BookmarkListEvent.kt index 2b14d4785..2fad460c8 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/BookmarkListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/BookmarkListEvent.kt @@ -24,7 +24,7 @@ import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer +import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.utils.TimeUtils @Immutable @@ -200,7 +200,7 @@ class BookmarkListEvent( if (tags.any { it.size > 1 && it[0] == "alt" }) { tags } else { - tags + AltTagSerializer.toTagArray(ALT) + tags + AltTag.assemble(ALT) } signer.sign(createdAt, KIND, newTags, content, onReady) @@ -224,7 +224,7 @@ class BookmarkListEvent( events?.forEach { tags.add(arrayOf("e", it)) } users?.forEach { tags.add(arrayOf("p", it)) } addresses?.forEach { tags.add(arrayOf("a", it.toTag())) } - tags.add(AltTagSerializer.toTagArray(ALT)) + tags.add(AltTag.assemble(ALT)) createPrivateTags(privEvents, privUsers, privAddresses, signer) { content -> signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/GeneralListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/GeneralListEvent.kt index 1d4554054..07920511c 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/GeneralListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/GeneralListEvent.kt @@ -28,6 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEvents import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohashes +import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUserIds import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUsers import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.toImmutableSet @@ -48,6 +49,8 @@ abstract class GeneralListEvent( fun bookmarkedPeople() = taggedUsers() + fun bookmarkedPeopleIds() = taggedUserIds() + fun name() = tags.firstOrNull { it.size > 1 && it[0] == "name" }?.get(1) fun title() = tags.firstOrNull { it.size > 1 && it[0] == "title" }?.get(1) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/MuteListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/MuteListEvent.kt index b758d1c58..23d91f889 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/MuteListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/MuteListEvent.kt @@ -24,7 +24,7 @@ import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.BaseReplaceableEvent.Companion.FIXED_D_TAG import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer +import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.pointerSizeInBytes @@ -226,65 +226,45 @@ class MuteListEvent( fun removeWord( earlierVersion: MuteListEvent, word: String, - isPrivate: Boolean, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (MuteListEvent) -> Unit, - ) = removeTag(earlierVersion, "word", word, isPrivate, signer, createdAt, onReady) + ) = removeTag(earlierVersion, "word", word, signer, createdAt, onReady) fun removeUser( earlierVersion: MuteListEvent, pubKeyHex: String, - isPrivate: Boolean, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (MuteListEvent) -> Unit, - ) = removeTag(earlierVersion, "p", pubKeyHex, isPrivate, signer, createdAt, onReady) + ) = removeTag(earlierVersion, "p", pubKeyHex, signer, createdAt, onReady) fun removeTag( earlierVersion: MuteListEvent, key: String, tag: String, - isPrivate: Boolean, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (MuteListEvent) -> Unit, ) { - earlierVersion.isTagged(key, tag, isPrivate, signer) { isTagged -> - if (isTagged) { - if (isPrivate) { - earlierVersion.privateTagsOrEmpty(signer) { privateTags -> - encryptTags( - privateTags = - privateTags - .filter { it.size > 1 && !(it[0] == key && it[1] == tag) } - .toTypedArray(), - signer = signer, - ) { encryptedTags -> - create( - content = encryptedTags, - tags = - earlierVersion.tags - .filter { it.size > 1 && !(it[0] == key && it[1] == tag) } - .toTypedArray(), - signer = signer, - createdAt = createdAt, - onReady = onReady, - ) - } - } - } else { - create( - content = earlierVersion.content, - tags = - earlierVersion.tags - .filter { it.size > 1 && !(it[0] == key && it[1] == tag) } - .toTypedArray(), - signer = signer, - createdAt = createdAt, - onReady = onReady, - ) - } + earlierVersion.privateTagsOrEmpty(signer) { privateTags -> + encryptTags( + privateTags = + privateTags + .filter { it.size > 1 && !(it[0] == key && it[1] == tag) } + .toTypedArray(), + signer = signer, + ) { encryptedTags -> + create( + content = encryptedTags, + tags = + earlierVersion.tags + .filter { it.size > 1 && !(it[0] == key && it[1] == tag) } + .toTypedArray(), + signer = signer, + createdAt = createdAt, + onReady = onReady, + ) } } } @@ -300,7 +280,7 @@ class MuteListEvent( if (tags.any { it.size > 1 && it[0] == "alt" }) { tags } else { - tags + AltTagSerializer.toTagArray(ALT) + tags + AltTag.assemble(ALT) } signer.sign(createdAt, KIND, newTags, content, onReady) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/Nip51PrivateTags.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/Nip51PrivateTags.kt new file mode 100644 index 000000000..78a72d1f8 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/Nip51PrivateTags.kt @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip51Lists + +import com.fasterxml.jackson.module.kotlin.readValue +import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner + +class Nip51PrivateTags { + companion object { + fun decode(content: String) = EventMapper.mapper.readValue>>(content) + + fun encode(privateTags: Array>) = EventMapper.mapper.writeValueAsString(privateTags) + + fun decrypt( + content: String, + signer: NostrSigner, + onReady: (Array>) -> Unit, + ) { + signer.decrypt(content, signer.pubKey) { + onReady(decode(it)) + } + } + + fun encryptNip04( + privateTags: Array>? = null, + signer: NostrSigner, + onReady: (String) -> Unit, + ) = signer.nip04Encrypt( + if (privateTags.isNullOrEmpty()) "" else encode(privateTags), + signer.pubKey, + onReady, + ) + + fun encryptNip44( + privateTags: Array>? = null, + signer: NostrSigner, + onReady: (String) -> Unit, + ) = signer.nip44Encrypt( + if (privateTags.isNullOrEmpty()) "" else encode(privateTags), + signer.pubKey, + onReady, + ) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/PeopleListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/PeopleListEvent.kt index c1773f592..809825853 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/PeopleListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/PeopleListEvent.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.quartz.nip51Lists import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer +import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.pointerSizeInBytes @@ -277,65 +277,45 @@ class PeopleListEvent( fun removeWord( earlierVersion: PeopleListEvent, word: String, - isPrivate: Boolean, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (PeopleListEvent) -> Unit, - ) = removeTag(earlierVersion, "word", word, isPrivate, signer, createdAt, onReady) + ) = removeTag(earlierVersion, "word", word, signer, createdAt, onReady) fun removeUser( earlierVersion: PeopleListEvent, pubKeyHex: String, - isPrivate: Boolean, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (PeopleListEvent) -> Unit, - ) = removeTag(earlierVersion, "p", pubKeyHex, isPrivate, signer, createdAt, onReady) + ) = removeTag(earlierVersion, "p", pubKeyHex, signer, createdAt, onReady) fun removeTag( earlierVersion: PeopleListEvent, key: String, tag: String, - isPrivate: Boolean, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (PeopleListEvent) -> Unit, ) { - earlierVersion.isTagged(key, tag, isPrivate, signer) { isTagged -> - if (isTagged) { - if (isPrivate) { - earlierVersion.privateTagsOrEmpty(signer) { privateTags -> - encryptTags( - privateTags = - privateTags - .filter { it.size > 1 && !(it[0] == key && it[1] == tag) } - .toTypedArray(), - signer = signer, - ) { encryptedTags -> - create( - content = encryptedTags, - tags = - earlierVersion.tags - .filter { it.size > 1 && !(it[0] == key && it[1] == tag) } - .toTypedArray(), - signer = signer, - createdAt = createdAt, - onReady = onReady, - ) - } - } - } else { - create( - content = earlierVersion.content, - tags = - earlierVersion.tags - .filter { it.size > 1 && !(it[0] == key && it[1] == tag) } - .toTypedArray(), - signer = signer, - createdAt = createdAt, - onReady = onReady, - ) - } + earlierVersion.privateTagsOrEmpty(signer) { privateTags -> + encryptTags( + privateTags = + privateTags + .filter { it.size > 1 && !(it[0] == key && it[1] == tag) } + .toTypedArray(), + signer = signer, + ) { encryptedTags -> + create( + content = encryptedTags, + tags = + earlierVersion.tags + .filter { it.size > 1 && !(it[0] == key && it[1] == tag) } + .toTypedArray(), + signer = signer, + createdAt = createdAt, + onReady = onReady, + ) } } } @@ -351,7 +331,7 @@ class PeopleListEvent( if (tags.any { it.size > 1 && it[0] == "alt" }) { tags } else { - tags + AltTagSerializer.toTagArray(ALT) + tags + AltTag.assemble(ALT) } signer.sign(createdAt, KIND, newTags, content, onReady) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/PinListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/PinListEvent.kt index 6b7633e59..1540c1048 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/PinListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/PinListEvent.kt @@ -24,7 +24,7 @@ import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer +import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.utils.TimeUtils @Immutable @@ -50,7 +50,7 @@ class PinListEvent( ) { val tags = mutableListOf>() pins.forEach { tags.add(arrayOf("pin", it)) } - tags.add(AltTagSerializer.toTagArray(ALT)) + tags.add(AltTag.assemble(ALT)) signer.sign(createdAt, KIND, tags.toTypedArray(), "", onReady) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/PrivateTagArrayEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/PrivateTagArrayEvent.kt index 85285b0a1..5dbe05ff7 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/PrivateTagArrayEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/PrivateTagArrayEvent.kt @@ -22,10 +22,8 @@ package com.vitorpamplona.quartz.nip51Lists import android.util.Log import androidx.compose.runtime.Immutable -import com.fasterxml.jackson.module.kotlin.readValue import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent -import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.pointerSizeInBytes @@ -65,8 +63,8 @@ abstract class PrivateTagArrayEvent( } try { - signer.decrypt(content, pubKey) { - privateTagsCache = EventMapper.mapper.readValue>>(it) + Nip51PrivateTags.decrypt(content, signer) { + privateTagsCache = it privateTagsCache?.let { onReady(it) } } } catch (e: Throwable) { @@ -80,7 +78,7 @@ abstract class PrivateTagArrayEvent( onReady: (content: String) -> Unit, ) { privateTags(signer) { privateTags -> - encryptTags( + Nip51PrivateTags.encryptNip04( privateTags = change(privateTags), signer = signer, ) { encryptedTags -> @@ -99,7 +97,7 @@ abstract class PrivateTagArrayEvent( ) { if (toPrivate) { current.privateTags(signer) { privateTags -> - encryptTags( + Nip51PrivateTags.encryptNip04( privateTags = privateTags.plus(newTag), signer = signer, ) { encryptedTags -> @@ -120,7 +118,7 @@ abstract class PrivateTagArrayEvent( ) { if (toPrivate) { current.privateTags(signer) { privateTags -> - encryptTags( + Nip51PrivateTags.encryptNip04( privateTags = privateTags.plus(newTag), signer = signer, ) { encryptedTags -> @@ -170,7 +168,7 @@ abstract class PrivateTagArrayEvent( onReady: (content: String, tags: Array>) -> Unit, ) { current.privateTags(signer) { privateTags -> - encryptTags( + Nip51PrivateTags.encryptNip04( privateTags = privateTags.replaceAll(oldTagStartsWith, newTag), signer = signer, ) { encryptedTags -> @@ -187,7 +185,7 @@ abstract class PrivateTagArrayEvent( onReady: (content: String, tags: Array>) -> Unit, ) { current.privateTags(signer) { privateTags -> - encryptTags( + Nip51PrivateTags.encryptNip04( privateTags = privateTags.remove(oldTagStartsWith), signer = signer, ) { encryptedTags -> @@ -203,7 +201,7 @@ abstract class PrivateTagArrayEvent( onReady: (content: String, tags: Array>) -> Unit, ) { current.privateTags(signer) { privateTags -> - encryptTags( + Nip51PrivateTags.encryptNip04( privateTags = privateTags.remove(oldTagStartsWith), signer = signer, ) { encryptedTags -> @@ -226,7 +224,7 @@ abstract class PrivateTagArrayEvent( onReady: (content: String, tags: Array>) -> Unit, ) { current.privateTags(signer) { privateTags -> - encryptTags( + Nip51PrivateTags.encryptNip04( privateTags = privateTags.remove(oldTagStartsWith), signer = signer, ) { encryptedTags -> @@ -241,7 +239,7 @@ abstract class PrivateTagArrayEvent( signer: NostrSigner, onReady: (content: String, tags: Array>) -> Unit, ) { - encryptTags( + Nip51PrivateTags.encryptNip04( privateTags = arrayOf(newTag), signer = signer, ) { encryptedTags -> @@ -257,15 +255,5 @@ abstract class PrivateTagArrayEvent( ) { onReady("", arrayOf(arrayOf("d", dTag), newTag)) } - - fun encryptTags( - privateTags: Array>? = null, - signer: NostrSigner, - onReady: (String) -> Unit, - ) = signer.nip04Encrypt( - if (privateTags.isNullOrEmpty()) "" else EventMapper.mapper.writeValueAsString(privateTags), - signer.pubKey, - onReady, - ) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/RelaySetEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/RelaySetEvent.kt index f8da08bbf..59166d911 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/RelaySetEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/RelaySetEvent.kt @@ -24,7 +24,7 @@ import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer +import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.utils.TimeUtils @Immutable @@ -52,7 +52,7 @@ class RelaySetEvent( ) { val tags = mutableListOf>() relays.forEach { tags.add(arrayOf("r", it)) } - tags.add(AltTagSerializer.toTagArray(ALT)) + tags.add(AltTag.assemble(ALT)) signer.sign(createdAt, KIND, tags.toTypedArray(), "", onReady) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/TagArrayExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/TagArrayExt.kt index 06ac817dd..2ccfca792 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/TagArrayExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/TagArrayExt.kt @@ -27,9 +27,9 @@ inline fun TagArray.filterToArray(predicate: (Array) -> Boolean): TagArr inline fun TagArray.remove(predicate: (Array) -> Boolean): TagArray = filterNotTo(ArrayList(this.size), predicate).toTypedArray() -inline fun TagArray.remove(startsWith: Array): TagArray = filterNotTo(ArrayList(this.size), { it.startsWith(startsWith) }).toTypedArray() +fun TagArray.remove(startsWith: Array): TagArray = filterNotTo(ArrayList(this.size), { it.startsWith(startsWith) }).toTypedArray() -inline fun TagArray.replaceAll( +fun TagArray.replaceAll( startsWith: Array, newElement: Array, ): TagArray = filterNotTo(ArrayList(this.size), { it.startsWith(startsWith) }).plusElement(newElement).toTypedArray() diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip52Calendar/CalendarDateSlotEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip52Calendar/CalendarDateSlotEvent.kt index e07f9ba26..77d97fa42 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip52Calendar/CalendarDateSlotEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip52Calendar/CalendarDateSlotEvent.kt @@ -25,7 +25,7 @@ import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent import com.vitorpamplona.quartz.nip01Core.core.firstTagValue import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer +import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.utils.TimeUtils @Immutable @@ -55,7 +55,7 @@ class CalendarDateSlotEvent( createdAt: Long = TimeUtils.now(), onReady: (CalendarDateSlotEvent) -> Unit, ) { - val tags = arrayOf(AltTagSerializer.toTagArray(ALT)) + val tags = arrayOf(AltTag.assemble(ALT)) signer.sign(createdAt, KIND, tags, "", onReady) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip52Calendar/CalendarEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip52Calendar/CalendarEvent.kt index 8f9cd7f1e..0bcfd8bcc 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip52Calendar/CalendarEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip52Calendar/CalendarEvent.kt @@ -24,7 +24,7 @@ import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer +import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.utils.TimeUtils @Immutable @@ -45,7 +45,7 @@ class CalendarEvent( createdAt: Long = TimeUtils.now(), onReady: (CalendarEvent) -> Unit, ) { - val tags = arrayOf(AltTagSerializer.toTagArray(ALT)) + val tags = arrayOf(AltTag.assemble(ALT)) signer.sign(createdAt, KIND, tags, "", onReady) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip52Calendar/CalendarRSVPEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip52Calendar/CalendarRSVPEvent.kt index cd612d77f..0be0ed45e 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip52Calendar/CalendarRSVPEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip52Calendar/CalendarRSVPEvent.kt @@ -25,7 +25,7 @@ import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent import com.vitorpamplona.quartz.nip01Core.core.firstTagValue import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer +import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.utils.TimeUtils @Immutable @@ -57,7 +57,7 @@ class CalendarRSVPEvent( createdAt: Long = TimeUtils.now(), onReady: (CalendarRSVPEvent) -> Unit, ) { - val tags = arrayOf(AltTagSerializer.toTagArray(ALT)) + val tags = arrayOf(AltTag.assemble(ALT)) signer.sign(createdAt, KIND, tags, "", onReady) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip52Calendar/CalendarTimeSlotEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip52Calendar/CalendarTimeSlotEvent.kt index 9f716ed77..887f8bbdf 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip52Calendar/CalendarTimeSlotEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip52Calendar/CalendarTimeSlotEvent.kt @@ -26,7 +26,7 @@ import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent import com.vitorpamplona.quartz.nip01Core.core.firstTagValue import com.vitorpamplona.quartz.nip01Core.core.firstTagValueAsLong import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer +import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.utils.TimeUtils @Immutable @@ -62,7 +62,7 @@ class CalendarTimeSlotEvent( createdAt: Long = TimeUtils.now(), onReady: (CalendarTimeSlotEvent) -> Unit, ) { - val tags = arrayOf(AltTagSerializer.toTagArray(ALT)) + val tags = arrayOf(AltTag.assemble(ALT)) signer.sign(createdAt, KIND, tags, "", onReady) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/LiveActivitiesChatMessageEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/LiveActivitiesChatMessageEvent.kt deleted file mode 100644 index cc2c5468d..000000000 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/LiveActivitiesChatMessageEvent.kt +++ /dev/null @@ -1,114 +0,0 @@ -/** - * Copyright (c) 2024 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.quartz.nip53LiveActivities - -import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag -import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohashMipMap -import com.vitorpamplona.quartz.nip10Notes.BaseTextNoteEvent -import com.vitorpamplona.quartz.nip19Bech32.parse -import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrl -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer -import com.vitorpamplona.quartz.nip36SensitiveContent.ContentWarningSerializer -import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetup -import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetupSerializer -import com.vitorpamplona.quartz.nip57Zaps.zapraiser.ZapRaiserSerializer -import com.vitorpamplona.quartz.nip92IMeta.IMetaTag -import com.vitorpamplona.quartz.nip92IMeta.Nip92MediaAttachments -import com.vitorpamplona.quartz.utils.TimeUtils - -@Immutable -class LiveActivitiesChatMessageEvent( - id: HexKey, - pubKey: HexKey, - createdAt: Long, - tags: Array>, - content: String, - sig: HexKey, -) : BaseTextNoteEvent(id, pubKey, createdAt, KIND, tags, content, sig) { - private fun innerActivity() = - tags.firstOrNull { it.size > 3 && it[0] == "a" && it[3] == "root" } - ?: tags.firstOrNull { it.size > 1 && it[0] == "a" } - - private fun activityHex() = innerActivity()?.getOrNull(1) - - fun activity() = - innerActivity()?.let { - if (it.size > 1) { - ATag.parse(it[1], it.getOrNull(2)) - } else { - null - } - } - - override fun markedReplyTos() = super.markedReplyTos().minus(activityHex() ?: "") - - override fun unMarkedReplyTos() = super.markedReplyTos().minus(activityHex() ?: "") - - companion object { - const val KIND = 1311 - const val ALT = "Live activity chat message" - - fun create( - message: String, - activity: ATag, - replyTos: List? = null, - mentions: List? = null, - zapReceiver: List? = null, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - markAsSensitive: Boolean, - zapRaiserAmount: Long?, - geohash: String? = null, - imetas: List? = null, - emojis: List? = null, - isDraft: Boolean, - onReady: (LiveActivitiesChatMessageEvent) -> Unit, - ) { - val content = message - val tags = - mutableListOf( - arrayOf("a", activity.toTag(), "", "root"), - ) - replyTos?.forEach { tags.add(arrayOf("e", it)) } - mentions?.forEach { tags.add(arrayOf("p", it)) } - zapReceiver?.forEach { tags.add(ZapSplitSetupSerializer.toTagArray(it)) } - zapRaiserAmount?.let { tags.add(ZapRaiserSerializer.toTagArray(it)) } - if (markAsSensitive) { - tags.add(ContentWarningSerializer.toTagArray()) - } - geohash?.let { tags.addAll(geohashMipMap(it)) } - imetas?.forEach { - tags.add(Nip92MediaAttachments.createTag(it)) - } - emojis?.forEach { tags.add(it.toTagArray()) } - tags.add(AltTagSerializer.toTagArray(ALT)) - - if (isDraft) { - signer.assembleRumor(createdAt, KIND, tags.toTypedArray(), content, onReady) - } else { - signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady) - } - } - } -} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/LiveActivitiesEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/LiveActivitiesEvent.kt deleted file mode 100644 index 3469e9ff5..000000000 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/LiveActivitiesEvent.kt +++ /dev/null @@ -1,93 +0,0 @@ -/** - * Copyright (c) 2024 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.quartz.nip53LiveActivities - -import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.experimental.audio.Participant -import com.vitorpamplona.quartz.nip01Core.HexKey -import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip10Notes.PTag -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer -import com.vitorpamplona.quartz.utils.TimeUtils - -@Immutable -class LiveActivitiesEvent( - id: HexKey, - pubKey: HexKey, - createdAt: Long, - tags: Array>, - content: String, - sig: HexKey, -) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) { - fun title() = tags.firstOrNull { it.size > 1 && it[0] == "title" }?.get(1) - - fun summary() = tags.firstOrNull { it.size > 1 && it[0] == "summary" }?.get(1) - - fun image() = tags.firstOrNull { it.size > 1 && it[0] == "image" }?.get(1) - - fun streaming() = tags.firstOrNull { it.size > 1 && it[0] == "streaming" }?.get(1) - - fun starts() = tags.firstOrNull { it.size > 1 && it[0] == "starts" }?.get(1)?.toLongOrNull() - - fun ends() = tags.firstOrNull { it.size > 1 && it[0] == "ends" }?.get(1) - - fun status() = checkStatus(tags.firstOrNull { it.size > 1 && it[0] == "status" }?.get(1)) - - fun currentParticipants() = tags.firstOrNull { it.size > 1 && it[0] == "current_participants" }?.get(1) - - fun totalParticipants() = tags.firstOrNull { it.size > 1 && it[0] == "total_participants" }?.get(1) - - fun participants() = tags.filter { it.size > 1 && it[0] == "p" }.map { Participant(it[1], it.getOrNull(3)) } - - fun hasHost() = tags.any { it.size > 3 && it[0] == "p" && it[3].equals("Host", true) } - - fun host() = tags.firstOrNull { it.size > 3 && it[0] == "p" && it[3].equals("Host", true) }?.get(1) - - fun hosts() = tags.filter { it.size > 3 && it[0] == "p" && it[3].equals("Host", true) }.map { PTag(it[1], it.getOrNull(2)) } - - fun checkStatus(eventStatus: String?): String? = - if (eventStatus == STATUS_LIVE && createdAt < TimeUtils.eightHoursAgo()) { - STATUS_ENDED - } else { - eventStatus - } - - fun participantsIntersect(keySet: Set): Boolean = keySet.contains(pubKey) || tags.any { it.size > 1 && it[0] == "p" && it[1] in keySet } - - companion object { - const val KIND = 30311 - const val ALT = "Live activity event" - - const val STATUS_LIVE = "live" - const val STATUS_PLANNED = "planned" - const val STATUS_ENDED = "ended" - - fun create( - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (LiveActivitiesEvent) -> Unit, - ) { - val tags = arrayOf(AltTagSerializer.toTagArray(ALT)) - signer.sign(createdAt, KIND, tags, "", onReady) - } - } -} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/chat/LiveActivitiesChatMessageEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/chat/LiveActivitiesChatMessageEvent.kt new file mode 100644 index 000000000..c1ab8889d --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/chat/LiveActivitiesChatMessageEvent.kt @@ -0,0 +1,85 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip53LiveActivities.chat + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.EventHintBundle +import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent +import com.vitorpamplona.quartz.utils.TimeUtils + +@Immutable +class LiveActivitiesChatMessageEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : BaseThreadedEvent(id, pubKey, createdAt, KIND, tags, content, sig) { + private fun activityHex() = tags.firstNotNullOfOrNull(ATag::parseAddress) + + fun activity() = tags.firstNotNullOfOrNull(ATag::parse) + + override fun markedReplyTos() = super.markedReplyTos().minus(activityHex() ?: "") + + override fun unmarkedReplyTos() = super.markedReplyTos().minus(activityHex() ?: "") + + companion object { + const val KIND = 1311 + const val ALT = "Live activity chat message" + + fun reply( + post: String, + replyingTo: EventHintBundle, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, post, createdAt) { + replyingTo.event.activity()?.let { activity(it) } + reply(replyingTo) + initializer() + } + + fun message( + post: String, + activity: EventHintBundle, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, post, createdAt) { + activity(activity) + initializer() + } + + fun message( + post: String, + activity: ATag, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, post, createdAt) { + activity(activity) + initializer() + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/chat/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/chat/TagArrayBuilderExt.kt new file mode 100644 index 000000000..dc1ea675c --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/chat/TagArrayBuilderExt.kt @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip53LiveActivities.chat + +import com.vitorpamplona.quartz.nip01Core.EventHintBundle +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.nip01Core.tags.people.pTag +import com.vitorpamplona.quartz.nip01Core.tags.people.pTags +import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent + +fun TagArrayBuilder.activity(rep: ATag) = addUnique(rep.toATagArray()) + +fun TagArrayBuilder.activity(rep: EventHintBundle) = addUnique(rep.toATag().toATagArray()) + +fun TagArrayBuilder.reply(rep: EventHintBundle) = addUnique(rep.toMarkedETag(MarkedETag.MARKER.REPLY).toTagArray()) + +fun TagArrayBuilder.notify(list: List) = pTags(list) + +fun TagArrayBuilder.notify(pubkey: PTag) = pTag(pubkey) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/LiveActivitiesEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/LiveActivitiesEvent.kt new file mode 100644 index 000000000..19dd12bf1 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/LiveActivitiesEvent.kt @@ -0,0 +1,103 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip53LiveActivities.streaming + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent +import com.vitorpamplona.quartz.nip01Core.core.any +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip23LongContent.tags.ImageTag +import com.vitorpamplona.quartz.nip23LongContent.tags.SummaryTag +import com.vitorpamplona.quartz.nip23LongContent.tags.TitleTag +import com.vitorpamplona.quartz.nip31Alts.AltTag +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.CurrentParticipantsTag +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.EndsTag +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.ParticipantTag +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.RelayListTag +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.StartsTag +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.StatusTag +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.StreamingTag +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.TotalParticipantsTag +import com.vitorpamplona.quartz.utils.TimeUtils + +@Immutable +class LiveActivitiesEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) { + fun title() = tags.firstNotNullOfOrNull(TitleTag::parse) + + fun summary() = tags.firstNotNullOfOrNull(SummaryTag::parse) + + fun image() = tags.firstNotNullOfOrNull(ImageTag::parse) + + fun streaming() = tags.firstNotNullOfOrNull(StreamingTag::parse) + + fun starts() = tags.firstNotNullOfOrNull(StartsTag::parse) + + fun ends() = tags.firstNotNullOfOrNull(EndsTag::parse) + + fun status() = checkStatus(tags.firstNotNullOfOrNull(StatusTag::parse)) + + fun currentParticipants() = tags.firstNotNullOfOrNull(CurrentParticipantsTag::parse) + + fun totalParticipants() = tags.firstNotNullOfOrNull(TotalParticipantsTag::parse) + + fun participants() = tags.mapNotNull(ParticipantTag::parse) + + fun relays() = tags.mapNotNull(RelayListTag::parse) + + fun allRelayUrls() = tags.mapNotNull(RelayListTag::parse).map { it.relayUrls }.flatten() + + fun hasHost() = tags.any(ParticipantTag::isHost) + + fun host() = tags.firstNotNullOfOrNull(ParticipantTag::parseHost) + + fun hosts() = tags.mapNotNull(ParticipantTag::parseHost) + + fun checkStatus(eventStatus: String?): String? = + if (eventStatus == StatusTag.STATUS.LIVE.code && createdAt < TimeUtils.eightHoursAgo()) { + StatusTag.STATUS.ENDED.code + } else { + eventStatus + } + + fun participantsIntersect(keySet: Set): Boolean = keySet.contains(pubKey) || tags.any(ParticipantTag::isIn, keySet) + + companion object { + const val KIND = 30311 + const val ALT = "Live activity event" + + fun create( + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (LiveActivitiesEvent) -> Unit, + ) { + val tags = arrayOf(AltTag.assemble(ALT)) + signer.sign(createdAt, KIND, tags, "", onReady) + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/tags/CurrentParticipantsTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/tags/CurrentParticipantsTag.kt new file mode 100644 index 000000000..934a4637b --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/tags/CurrentParticipantsTag.kt @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags + +class CurrentParticipantsTag { + companion object { + const val TAG_NAME = "current_participants" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): Int? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1].toIntOrNull() + } + + @JvmStatic + fun assemble(participantCount: Int) = arrayOf(TAG_NAME, participantCount.toString()) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/tags/EndsTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/tags/EndsTag.kt new file mode 100644 index 000000000..0597b1d54 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/tags/EndsTag.kt @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags + +class EndsTag { + companion object { + const val TAG_NAME = "ends" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): Long? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1].toLongOrNull() + } + + @JvmStatic + fun assemble(timestamp: Long) = arrayOf(TAG_NAME, timestamp.toString()) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/tags/ParticipantTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/tags/ParticipantTag.kt new file mode 100644 index 000000000..565f5c02e --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/tags/ParticipantTag.kt @@ -0,0 +1,91 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.Tag +import com.vitorpamplona.quartz.nip01Core.core.isNotName +import com.vitorpamplona.quartz.nip01Core.tags.people.PubKeyReferenceTag +import com.vitorpamplona.quartz.utils.arrayOfNotNull + +enum class ROLE( + val code: String, +) { + HOST("host"), + SPEAKER("speaker"), +} + +@Immutable +data class ParticipantTag( + override val pubKey: String, + override val relayHint: String?, + val role: String?, + val proof: String?, +) : PubKeyReferenceTag { + companion object { + const val TAG_NAME = "p" + const val TAG_SIZE = 2 + + fun isIn( + tag: Array, + keys: Set, + ) = tag.size >= TAG_SIZE && tag[0] == TAG_NAME && tag[1] in keys + + @JvmStatic + fun isHost(tag: Tag): Boolean { + if (tag.isNotName(TAG_NAME, TAG_SIZE)) return false + if (tag[1].length != 64) return false + if (tag.getOrNull(3).equals(ROLE.HOST.code)) return true + return false + } + + @JvmStatic + fun parse(tag: Tag): ParticipantTag? { + if (tag.isNotName(TAG_NAME, TAG_SIZE)) return null + if (tag[1].length != 64) return null + return ParticipantTag(tag[1], tag.getOrNull(2), tag.getOrNull(3), tag.getOrNull(4)) + } + + @JvmStatic + fun parseHost(tag: Tag): ParticipantTag? { + if (tag.isNotName(TAG_NAME, TAG_SIZE)) return null + if (tag[1].length != 64) return null + if (!tag.getOrNull(3).equals(ROLE.HOST.code)) return null + return ParticipantTag(tag[1], tag.getOrNull(2), tag.getOrNull(3), tag.getOrNull(4)) + } + + @JvmStatic + fun parseKey(tag: Tag): String? { + if (tag.isNotName(TAG_NAME, TAG_SIZE)) return null + if (tag[1].length != 64) return null + return tag[1] + } + + @JvmStatic + fun assemble( + pubkey: HexKey, + relayHint: String?, + role: String?, + proof: String?, + ) = arrayOfNotNull(TAG_NAME, pubkey, relayHint, role, proof) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/tags/RelayListTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/tags/RelayListTag.kt new file mode 100644 index 000000000..fda64e467 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/tags/RelayListTag.kt @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags + +class RelayListTag( + val relayUrls: List, +) { + companion object { + const val TAG_NAME = "relays" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): RelayListTag? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + val relays = + tag.mapIndexedNotNull { index, s -> + if (index == 0) null else s + } + return RelayListTag(relays) + } + + @JvmStatic + fun assemble(urls: List) = arrayOf(TAG_NAME) + urls.toTypedArray() + + @JvmStatic + fun assemble(tag: RelayListTag) = assemble(tag.relayUrls) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/tags/StartsTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/tags/StartsTag.kt new file mode 100644 index 000000000..8c6cb4488 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/tags/StartsTag.kt @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags + +class StartsTag { + companion object { + const val TAG_NAME = "starts" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): Long? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1].toLongOrNull() + } + + @JvmStatic + fun assemble(timestamp: Long) = arrayOf(TAG_NAME, timestamp.toString()) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/tags/StatusTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/tags/StatusTag.kt new file mode 100644 index 000000000..d6e651b9e --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/tags/StatusTag.kt @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags + +class StatusTag { + enum class STATUS( + val code: String, + ) { + LIVE("live"), + PLANNED("planned"), + ENDED("ended"), + ; + + fun toTagArray() = assemble(this) + } + + companion object { + const val TAG_NAME = "status" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): String? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1] + } + + @JvmStatic + fun assemble(status: STATUS) = arrayOf(TAG_NAME, status.code) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/tags/StreamingTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/tags/StreamingTag.kt new file mode 100644 index 000000000..7303e1a44 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/tags/StreamingTag.kt @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags + +class StreamingTag { + companion object { + const val TAG_NAME = "streaming" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): String? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1] + } + + @JvmStatic + fun assemble(url: String) = arrayOf(TAG_NAME, url) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/tags/TotalParticipantsTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/tags/TotalParticipantsTag.kt new file mode 100644 index 000000000..52bbeac76 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/tags/TotalParticipantsTag.kt @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags + +class TotalParticipantsTag { + companion object { + const val TAG_NAME = "total_participants" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): Int? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1].toIntOrNull() + } + + @JvmStatic + fun assemble(participantCount: Int) = arrayOf(TAG_NAME, participantCount.toString()) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip54Wiki/WikiNoteEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip54Wiki/WikiNoteEvent.kt index 5369c1195..705a1501e 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip54Wiki/WikiNoteEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip54Wiki/WikiNoteEvent.kt @@ -25,9 +25,11 @@ import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address +import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags -import com.vitorpamplona.quartz.nip10Notes.BaseTextNoteEvent -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer +import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent +import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.utils.TimeUtils @Immutable @@ -38,11 +40,13 @@ class WikiNoteEvent( tags: Array>, content: String, sig: HexKey, -) : BaseTextNoteEvent(id, pubKey, createdAt, KIND, tags, content, sig), +) : BaseThreadedEvent(id, pubKey, createdAt, KIND, tags, content, sig), AddressableEvent { - override fun dTag() = tags.firstOrNull { it.size > 1 && it[0] == "d" }?.get(1) ?: "" + override fun dTag() = tags.dTag() - override fun address(relayHint: String?) = ATag(kind, pubKey, dTag(), relayHint) + override fun aTag(relayHint: String?) = ATag(kind, pubKey, dTag(), relayHint) + + override fun address() = Address(kind, pubKey, dTag()) override fun addressTag() = ATag.assembleATagId(kind, pubKey, dTag()) @@ -77,7 +81,7 @@ class WikiNoteEvent( replyTos?.forEach { tags.add(arrayOf("e", it)) } mentions?.forEach { tags.add(arrayOf("p", it)) } title?.let { tags.add(arrayOf("title", it)) } - tags.add(AltTagSerializer.toTagArray("Wiki Post: $title")) + tags.add(AltTag.assemble("Wiki Post: $title")) signer.sign(createdAt, KIND, tags.toTypedArray(), msg, onReady) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip56Reports/ReportEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip56Reports/ReportEvent.kt index 6123f73bc..4d95a7a2e 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip56Reports/ReportEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip56Reports/ReportEvent.kt @@ -25,7 +25,7 @@ import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer +import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.utils.TimeUtils @Immutable data class ReportedKey( @@ -100,10 +100,10 @@ class ReportEvent( var tags: Array> = arrayOf(reportPostTag, reportAuthorTag) if (reportedPost is AddressableEvent) { - tags += listOf(arrayOf("a", reportedPost.address().toTag())) + tags += listOf(arrayOf("a", reportedPost.aTag().toTag())) } - tags += listOf(AltTagSerializer.toTagArray("Report for ${type.name}")) + tags += listOf(AltTag.assemble("Report for ${type.name}")) signer.sign(createdAt, KIND, tags, content, onReady) } @@ -118,7 +118,7 @@ class ReportEvent( val content = "" val reportAuthorTag = arrayOf("p", reportedUser, type.name.lowercase()) - val alt = AltTagSerializer.toTagArray("Report for ${type.name}") + val alt = AltTag.assemble("Report for ${type.name}") val tags: Array> = arrayOf(reportAuthorTag, alt) signer.sign(createdAt, KIND, tags, content, onReady) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapEvent.kt index 016580a17..172f66121 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapEvent.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.quartz.nip57Zaps import android.util.Log import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.experimental.zapPolls.POLL_OPTION +import com.vitorpamplona.quartz.experimental.zapPolls.tags.PollOptionTag import com.vitorpamplona.quartz.lightning.LnInvoiceUtil import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Event @@ -76,7 +76,7 @@ class LnZapEvent( try { zapRequest ?.tags - ?.firstOrNull { it.size > 1 && it[0] == POLL_OPTION } + ?.firstOrNull { it.size > 1 && it[0] == PollOptionTag.TAG_NAME } ?.get(1) ?.toInt() } catch (e: Exception) { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapRequestEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapRequestEvent.kt index 10d3d07d8..38bed9b9e 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapRequestEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapRequestEvent.kt @@ -21,16 +21,16 @@ package com.vitorpamplona.quartz.nip57Zaps import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.experimental.zapPolls.POLL_OPTION +import com.vitorpamplona.quartz.experimental.zapPolls.tags.PollOptionTag import com.vitorpamplona.quartz.nip01Core.HexKey -import com.vitorpamplona.quartz.nip01Core.KeyPair import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.mapValues +import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.hexToByteArray import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer +import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.pointerSizeInBytes @@ -115,13 +115,13 @@ class LnZapRequestEvent( arrayOf("e", originalNote.id), arrayOf("p", toUserPubHex ?: originalNote.pubKey), arrayOf("relays") + relays, - AltTagSerializer.toTagArray(ALT), + AltTag.assemble(ALT), ) if (originalNote is AddressableEvent) { - tags = tags + listOf(arrayOf("a", originalNote.address().toTag())) + tags = tags + listOf(arrayOf("a", originalNote.aTag().toTag())) } if (pollOption != null && pollOption >= 0) { - tags = tags + listOf(arrayOf(POLL_OPTION, pollOption.toString())) + tags = tags + listOf(arrayOf(PollOptionTag.TAG_NAME, pollOption.toString())) } if (zapType == LnZapEvent.ZapType.ANONYMOUS) { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/PrivateZapRequestBuilder.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/PrivateZapRequestBuilder.kt index 9215487f5..8ee6fbf4c 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/PrivateZapRequestBuilder.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/PrivateZapRequestBuilder.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.quartz.nip57Zaps import android.util.Log import com.vitorpamplona.quartz.CryptoUtils -import com.vitorpamplona.quartz.nip01Core.KeyPair +import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.hexToByteArray import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync import com.vitorpamplona.quartz.nip01Core.toHexKey diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/splits/BaseZapSplitSetup.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/splits/BaseZapSplitSetup.kt index 55a4e1d81..e81c4094d 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/splits/BaseZapSplitSetup.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/splits/BaseZapSplitSetup.kt @@ -24,4 +24,8 @@ sealed interface BaseZapSplitSetup { val weight: Double fun mainId(): String + + companion object { + const val TAG_NAME = "zap" + } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/splits/EventExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/splits/EventExt.kt index 8aa6bbece..5c865e3af 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/splits/EventExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/splits/EventExt.kt @@ -21,9 +21,7 @@ package com.vitorpamplona.quartz.nip57Zaps.splits import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.core.hasTagWithContent -import com.vitorpamplona.quartz.nip01Core.core.mapTagged -fun Event.hasZapSplitSetup() = tags.hasTagWithContent("zap") +fun Event.hasZapSplitSetup() = tags.hasZapSplitSetup() -fun Event.zapSplitSetup(): List = tags.mapTagged("zap") { ZapSplitSetupParser.parse(it) } +fun Event.zapSplitSetup(): List = tags.zapSplitSetup() diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/splits/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/splits/TagArrayBuilderExt.kt new file mode 100644 index 000000000..6448b919b --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/splits/TagArrayBuilderExt.kt @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip57Zaps.splits + +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder + +fun TagArrayBuilder.zapSplit(zapSplit: BaseZapSplitSetup) = add(ZapSplitSetupSerializer.toTagArray(zapSplit)) + +fun TagArrayBuilder.zapSplits(zapSplits: List) { + addAll(zapSplits.map { ZapSplitSetupSerializer.toTagArray(it) }) +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/splits/TagArrayExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/splits/TagArrayExt.kt new file mode 100644 index 000000000..2775ce1d1 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/splits/TagArrayExt.kt @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip57Zaps.splits + +import com.vitorpamplona.quartz.nip01Core.core.TagArray +import com.vitorpamplona.quartz.nip01Core.core.hasTagWithContent +import com.vitorpamplona.quartz.nip01Core.core.mapTagged + +fun TagArray.hasZapSplitSetup() = this.hasTagWithContent(BaseZapSplitSetup.TAG_NAME) + +fun TagArray.zapSplitSetup(): List = this.mapTagged(BaseZapSplitSetup.TAG_NAME) { ZapSplitSetupParser.parse(it) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/splits/ZapSplitSetupParser.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/splits/ZapSplitSetupParser.kt index 292de0a4b..0d2e15abc 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/splits/ZapSplitSetupParser.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/splits/ZapSplitSetupParser.kt @@ -24,7 +24,7 @@ class ZapSplitSetupParser { companion object { @JvmStatic fun parse(tags: Array): BaseZapSplitSetup? { - require(tags[0] == "zap") + require(tags[0] == BaseZapSplitSetup.TAG_NAME) val isLnAddress = tags[1].contains("@") || tags[1].startsWith("LNURL", true) val weight = if (isLnAddress) 1.0 else (tags.getOrNull(3)?.toDoubleOrNull() ?: 0.0) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/splits/ZapSplitSetupSerializer.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/splits/ZapSplitSetupSerializer.kt index b10e15917..853c01a4f 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/splits/ZapSplitSetupSerializer.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/splits/ZapSplitSetupSerializer.kt @@ -25,8 +25,8 @@ class ZapSplitSetupSerializer { @JvmStatic fun toTagArray(zapSplit: BaseZapSplitSetup): Array = when (zapSplit) { - is ZapSplitSetupLnAddress -> arrayOf("zap", zapSplit.lnAddress) - is ZapSplitSetup -> arrayOf("zap", zapSplit.pubKeyHex, zapSplit.relay ?: "", zapSplit.weight.toString()) + is ZapSplitSetupLnAddress -> arrayOf(BaseZapSplitSetup.TAG_NAME, zapSplit.lnAddress) + is ZapSplitSetup -> arrayOf(BaseZapSplitSetup.TAG_NAME, zapSplit.pubKeyHex, zapSplit.relay ?: "", zapSplit.weight.toString()) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/zapraiser/EventExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/zapraiser/EventExt.kt index 12c5de261..414fa2183 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/zapraiser/EventExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/zapraiser/EventExt.kt @@ -21,6 +21,5 @@ package com.vitorpamplona.quartz.nip57Zaps.zapraiser import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.core.firstTagValueAsLong -fun Event.zapraiserAmount() = tags.firstTagValueAsLong("zapraiser") +fun Event.zapraiserAmount() = tags.zapraiserAmount() diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/zapraiser/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/zapraiser/TagArrayBuilderExt.kt new file mode 100644 index 000000000..d854525c2 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/zapraiser/TagArrayBuilderExt.kt @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip57Zaps.zapraiser + +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder + +fun TagArrayBuilder.zapraiser(amountInSats: Long) = addUnique(ZapRaiserTag.assemble(amountInSats)) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/zapraiser/ZapRaiserSerializer.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/zapraiser/TagArrayExt.kt similarity index 85% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/zapraiser/ZapRaiserSerializer.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/zapraiser/TagArrayExt.kt index f41ba3c1e..3f730960d 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/zapraiser/ZapRaiserSerializer.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/zapraiser/TagArrayExt.kt @@ -20,9 +20,7 @@ */ package com.vitorpamplona.quartz.nip57Zaps.zapraiser -class ZapRaiserSerializer { - companion object { - @JvmStatic - fun toTagArray(zapRaiserAmount: Long): Array = arrayOf("zapraiser", zapRaiserAmount.toString()) - } -} +import com.vitorpamplona.quartz.nip01Core.core.TagArray +import com.vitorpamplona.quartz.nip01Core.core.firstTagValueAsLong + +fun TagArray.zapraiserAmount() = this.firstTagValueAsLong(ZapRaiserTag.TAG_NAME) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/zapraiser/ZapRaiserTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/zapraiser/ZapRaiserTag.kt new file mode 100644 index 000000000..192ade51d --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/zapraiser/ZapRaiserTag.kt @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip57Zaps.zapraiser + +import com.vitorpamplona.quartz.utils.bytesUsedInMemory +import com.vitorpamplona.quartz.utils.pointerSizeInBytes + +class ZapRaiserTag( + val amountInSats: Long, +) { + fun countMemory(): Long = 1 * pointerSizeInBytes + amountInSats.bytesUsedInMemory() + + fun toTagArray() = assemble(amountInSats) + + companion object { + val TAG_NAME = "zapraiser" + + @JvmStatic + fun parse(tags: Array): ZapRaiserTag? { + require(tags[0] == TAG_NAME) + return tags[1].toLongOrNull()?.let { ZapRaiserTag(it) } + } + + @JvmStatic + fun assemble(amountInSats: Long) = arrayOf(TAG_NAME, amountInSats.toString()) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip58Badges/BadgeAwardEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip58Badges/BadgeAwardEvent.kt index 5b44a30f5..101f965ab 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip58Badges/BadgeAwardEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip58Badges/BadgeAwardEvent.kt @@ -24,6 +24,7 @@ import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedAddresses +import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUserIds import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUsers @Immutable @@ -37,6 +38,8 @@ class BadgeAwardEvent( ) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { fun awardees() = taggedUsers() + fun awardeeIds() = taggedUserIds() + fun awardDefinition() = taggedAddresses() companion object { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/Rumor.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/rumors/Rumor.kt similarity index 95% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/Rumor.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/rumors/Rumor.kt index 8eec17bd0..6202fb99e 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/Rumor.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/rumors/Rumor.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip59Giftwrap +package com.vitorpamplona.quartz.nip59Giftwrap.rumors import com.fasterxml.jackson.annotation.JsonProperty import com.vitorpamplona.quartz.EventFactory @@ -26,6 +26,7 @@ import com.vitorpamplona.quartz.nip01Core.EventHasher import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper +import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent class Rumor( val id: HexKey?, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/RumorDeserializer.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/rumors/RumorDeserializer.kt similarity index 97% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/RumorDeserializer.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/rumors/RumorDeserializer.kt index 9f6f1b829..f683e44e4 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/RumorDeserializer.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/rumors/RumorDeserializer.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip59Giftwrap +package com.vitorpamplona.quartz.nip59Giftwrap.rumors import com.fasterxml.jackson.core.JsonParser import com.fasterxml.jackson.databind.DeserializationContext diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/RumorSerializer.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/rumors/RumorSerializer.kt similarity index 97% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/RumorSerializer.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/rumors/RumorSerializer.kt index 4816f1cc5..a7fb1d0aa 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/RumorSerializer.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/rumors/RumorSerializer.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip59Giftwrap +package com.vitorpamplona.quartz.nip59Giftwrap.rumors import com.fasterxml.jackson.core.JsonGenerator import com.fasterxml.jackson.databind.SerializerProvider diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/SealedRumorEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/seals/SealedRumorEvent.kt similarity index 95% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/SealedRumorEvent.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/seals/SealedRumorEvent.kt index cb0dbf4b1..2fec9cbf3 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/SealedRumorEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/seals/SealedRumorEvent.kt @@ -18,13 +18,16 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip59Giftwrap +package com.vitorpamplona.quartz.nip59Giftwrap.seals import android.util.Log import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip59Giftwrap.HostStub +import com.vitorpamplona.quartz.nip59Giftwrap.WrappedEvent +import com.vitorpamplona.quartz.nip59Giftwrap.rumors.Rumor import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.pointerSizeInBytes diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/GiftWrapEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/wraps/GiftWrapEvent.kt similarity index 95% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/GiftWrapEvent.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/wraps/GiftWrapEvent.kt index a07795a71..b512fa852 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/GiftWrapEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/wraps/GiftWrapEvent.kt @@ -18,17 +18,19 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip59Giftwrap +package com.vitorpamplona.quartz.nip59Giftwrap.wraps import android.util.Log import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.HexKey -import com.vitorpamplona.quartz.nip01Core.KeyPair import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.firstTagValue +import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal import com.vitorpamplona.quartz.nip21UriScheme.toNostrUri +import com.vitorpamplona.quartz.nip59Giftwrap.HostStub +import com.vitorpamplona.quartz.nip59Giftwrap.WrappedEvent import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.pointerSizeInBytes diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip65RelayList/AdvertisedRelayListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip65RelayList/AdvertisedRelayListEvent.kt index 323ed575a..3cd3bc662 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip65RelayList/AdvertisedRelayListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip65RelayList/AdvertisedRelayListEvent.kt @@ -26,7 +26,7 @@ import com.vitorpamplona.quartz.nip01Core.core.BaseReplaceableEvent import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer +import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.utils.TimeUtils @Immutable @@ -125,7 +125,7 @@ class AdvertisedRelayListEvent( fun createTagArray(relays: List): Array> = relays .map(Companion::createRelayTag) - .plusElement(AltTagSerializer.toTagArray(ALT)) + .plusElement(AltTag.assemble(ALT)) .toTypedArray() fun create( diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip68Picture/IMetaTagBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip68Picture/IMetaTagBuilderExt.kt new file mode 100644 index 000000000..c6a4e421e --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip68Picture/IMetaTagBuilderExt.kt @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip68Picture + +import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip31Alts.AltTag +import com.vitorpamplona.quartz.nip36SensitiveContent.ContentWarningTag +import com.vitorpamplona.quartz.nip68Picture.tags.UserAnnotationTag +import com.vitorpamplona.quartz.nip92IMeta.IMetaTagBuilder +import com.vitorpamplona.quartz.nip94FileMetadata.tags.BlurhashTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.FallbackTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.HashTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.ImageTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.MagnetTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.MimeTypeTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.OriginalHashTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.ServiceTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.SizeTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.SummaryTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.ThumbTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.TorrentInfoHash + +/** + * Contains the IMeta tags that are used by Picture events. + */ +fun IMetaTagBuilder.magnet(uri: String) = add(MagnetTag.TAG_NAME, uri) + +fun IMetaTagBuilder.mimeType(mime: String) = add(MimeTypeTag.TAG_NAME, mime) + +fun IMetaTagBuilder.alt(alt: String) = add(AltTag.TAG_NAME, alt) + +fun IMetaTagBuilder.hash(hash: HexKey) = add(HashTag.TAG_NAME, hash) + +fun IMetaTagBuilder.size(size: Int) = add(SizeTag.TAG_NAME, size.toString()) + +fun IMetaTagBuilder.dims(dims: DimensionTag) = add(DimensionTag.TAG_NAME, dims.toString()) + +fun IMetaTagBuilder.blurhash(blurhash: String) = add(BlurhashTag.TAG_NAME, blurhash) + +fun IMetaTagBuilder.originalHash(originalHash: String) = add(OriginalHashTag.TAG_NAME, originalHash) + +fun IMetaTagBuilder.torrent(uri: String) = add(TorrentInfoHash.TAG_NAME, uri) + +fun IMetaTagBuilder.sensitiveContent(reason: String) = add(ContentWarningTag.TAG_NAME, reason) + +fun IMetaTagBuilder.image(imageUrl: HexKey) = add(ImageTag.TAG_NAME, imageUrl) + +fun IMetaTagBuilder.thumb(thumbUrl: HexKey) = add(ThumbTag.TAG_NAME, thumbUrl) + +fun IMetaTagBuilder.summary(summary: HexKey) = add(SummaryTag.TAG_NAME, summary) + +fun IMetaTagBuilder.fallback(fallback: HexKey) = add(FallbackTag.TAG_NAME, fallback) + +fun IMetaTagBuilder.service(service: HexKey) = add(ServiceTag.TAG_NAME, service) + +fun IMetaTagBuilder.userAnnotations(tag: UserAnnotationTag) = add(UserAnnotationTag.TAG_NAME, tag.toString()) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip68Picture/IMetaTagExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip68Picture/IMetaTagExt.kt new file mode 100644 index 000000000..a4a48131d --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip68Picture/IMetaTagExt.kt @@ -0,0 +1,74 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip68Picture + +import com.vitorpamplona.quartz.nip31Alts.AltTag +import com.vitorpamplona.quartz.nip36SensitiveContent.ContentWarningTag +import com.vitorpamplona.quartz.nip68Picture.tags.UserAnnotationTag +import com.vitorpamplona.quartz.nip92IMeta.IMetaTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.BlurhashTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.FallbackTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.HashTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.ImageTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.MagnetTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.MimeTypeTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.OriginalHashTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.ServiceTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.SizeTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.SummaryTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.ThumbTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.TorrentInfoHash + +/** + * Contains the IMeta tags that are used by Picture events. + */ +fun IMetaTag.magnet() = properties.get(MagnetTag.TAG_NAME) + +fun IMetaTag.mimeType() = properties.get(MimeTypeTag.TAG_NAME) + +fun IMetaTag.alt() = properties.get(AltTag.TAG_NAME) + +fun IMetaTag.hash() = properties.get(HashTag.TAG_NAME) + +fun IMetaTag.size() = properties.get(SizeTag.TAG_NAME) + +fun IMetaTag.dims() = properties.get(DimensionTag.TAG_NAME) + +fun IMetaTag.blurhash() = properties.get(BlurhashTag.TAG_NAME) + +fun IMetaTag.originalHash() = properties.get(OriginalHashTag.TAG_NAME) + +fun IMetaTag.torrent() = properties.get(TorrentInfoHash.TAG_NAME) + +fun IMetaTag.sensitiveContent() = properties.get(ContentWarningTag.TAG_NAME) + +fun IMetaTag.image() = properties.get(ImageTag.TAG_NAME) + +fun IMetaTag.thumb() = properties.get(ThumbTag.TAG_NAME) + +fun IMetaTag.summary() = properties.get(SummaryTag.TAG_NAME) + +fun IMetaTag.fallback() = properties.get(FallbackTag.TAG_NAME) + +fun IMetaTag.service() = properties.get(ServiceTag.TAG_NAME) + +fun IMetaTag.userAnnotations() = properties.get(UserAnnotationTag.TAG_NAME)?.mapNotNull { UserAnnotationTag.parse(it) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip68Picture/PictureEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip68Picture/PictureEvent.kt index 6654fa9b2..8aecc2bd9 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip68Picture/PictureEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip68Picture/PictureEvent.kt @@ -23,23 +23,26 @@ package com.vitorpamplona.quartz.nip68Picture import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag -import com.vitorpamplona.quartz.nip01Core.tags.events.ETag -import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohashMipMap -import com.vitorpamplona.quartz.nip01Core.tags.hashtags.buildHashtagTags -import com.vitorpamplona.quartz.nip10Notes.PTag -import com.vitorpamplona.quartz.nip10Notes.content.buildUrlRefs -import com.vitorpamplona.quartz.nip10Notes.content.findHashtags -import com.vitorpamplona.quartz.nip10Notes.content.findURLs +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.core.any +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip22Comments.RootScope -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer -import com.vitorpamplona.quartz.nip36SensitiveContent.ContentWarningSerializer -import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetup -import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetupSerializer -import com.vitorpamplona.quartz.nip57Zaps.zapraiser.ZapRaiserSerializer -import com.vitorpamplona.quartz.nip92IMeta.Nip92MediaAttachments.Companion.IMETA -import com.vitorpamplona.quartz.nip94FileMetadata.Dimension +import com.vitorpamplona.quartz.nip23LongContent.tags.TitleTag +import com.vitorpamplona.quartz.nip31Alts.alt +import com.vitorpamplona.quartz.nip92IMeta.imetas +import com.vitorpamplona.quartz.nip94FileMetadata.tags.BlurhashTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.FallbackTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.HashTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.ImageTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.MagnetTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.MimeTypeTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.ServiceTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.SizeTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.ThumbTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.TorrentInfoHash +import com.vitorpamplona.quartz.nip94FileMetadata.tags.UrlTag +import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent import com.vitorpamplona.quartz.utils.TimeUtils @Immutable @@ -52,29 +55,44 @@ class PictureEvent( sig: HexKey, ) : Event(id, pubKey, createdAt, KIND, tags, content, sig), RootScope { - fun mimeTypes() = tags.filter { it.size > 1 && it[0] == MIME_TYPE } + // --------------- + // current + // -------------- - fun hashes() = tags.filter { it.size > 1 && it[0] == HASH } + fun title() = tags.firstNotNullOfOrNull(TitleTag::parse) - fun title() = tags.firstOrNull { it.size > 1 && it[0] == TITLE }?.get(1) + /** old standard didnt use IMetas **/ + private fun url() = tags.firstNotNullOfOrNull(UrlTag::parse) - private fun url() = tags.firstOrNull { it.size > 1 && it[0] == PictureMeta.URL }?.get(1) + private fun urls() = tags.mapNotNull(UrlTag::parse) - private fun urls() = tags.filter { it.size > 1 && it[0] == PictureMeta.URL }.map { it[1] } + private fun mimeType() = tags.firstNotNullOfOrNull(MimeTypeTag::parse) - private fun mimeType() = tags.firstOrNull { it.size > 1 && it[0] == PictureMeta.MIME_TYPE }?.get(1) + private fun hash() = tags.firstNotNullOfOrNull(HashTag::parse) - private fun hash() = tags.firstOrNull { it.size > 1 && it[0] == PictureMeta.HASH }?.get(1) + private fun size() = tags.firstNotNullOfOrNull(SizeTag::parse) - private fun size() = tags.firstOrNull { it.size > 1 && it[0] == PictureMeta.FILE_SIZE }?.get(1) + private fun dimensions() = tags.firstNotNullOfOrNull(DimensionTag::parse) - private fun alt() = tags.firstOrNull { it.size > 1 && it[0] == PictureMeta.ALT }?.get(1) + private fun magnetURI() = tags.firstNotNullOfOrNull(MagnetTag::parse) - private fun dimensions() = tags.firstOrNull { it.size > 1 && it[0] == PictureMeta.DIMENSION }?.get(1)?.let { Dimension.parse(it) } + private fun torrentInfoHash() = tags.firstNotNullOfOrNull(TorrentInfoHash::parse) - private fun blurhash() = tags.firstOrNull { it.size > 1 && it[0] == PictureMeta.BLUR_HASH }?.get(1) + private fun blurhash() = tags.firstNotNullOfOrNull(BlurhashTag::parse) - private fun hasUrl() = tags.any { it.size > 1 && it[0] == PictureMeta.URL } + private fun hasUrl() = tags.any(UrlTag::isTag) + + private fun isOneOf(mimeTypes: Set) = tags.any(MimeTypeTag::isIn, mimeTypes) + + private fun image() = tags.firstNotNullOfOrNull(ImageTag::parse) + + private fun images() = tags.mapNotNull(ImageTag::parse) + + private fun thumb() = tags.firstNotNullOfOrNull(ThumbTag::parse) + + private fun service() = tags.firstNotNullOfOrNull(ServiceTag::parse) + + private fun fallbacks() = tags.mapNotNull(FallbackTag::parse) // hack to fix pablo's bug fun rootImage() = @@ -86,285 +104,46 @@ class PictureEvent( alt = alt(), hash = hash(), dimension = dimensions(), - size = size()?.toLongOrNull(), - fallback = emptyList(), + size = size(), + service = service(), + fallback = fallbacks(), annotations = emptyList(), ) } - fun imetaTags() = - tags - .map { tagArray -> - if (tagArray.size > 1 && tagArray[0] == IMETA) { - PictureMeta.parse(tagArray) - } else { - null - } - }.plus(rootImage()) - .filterNotNull() + fun imetaTags() = imetas().map { PictureMeta.parse(it) }.plus(rootImage()).filterNotNull() companion object { const val KIND = 20 const val ALT_DESCRIPTION = "List of pictures" - private const val MIME_TYPE = "m" - private const val HASH = "x" - private const val TITLE = "title" - - fun create( - url: String, - msg: String? = null, - title: String? = null, - mimeType: String? = null, - alt: String? = null, - hash: String? = null, - size: Long? = null, - dimensions: Dimension? = null, - blurhash: String? = null, - usersMentioned: Set = emptySet(), - addressesMentioned: Set = emptySet(), - eventsMentioned: Set = emptySet(), - geohash: String? = null, - zapReceiver: List? = null, - markAsSensitive: Boolean = false, - zapRaiserAmount: Long? = null, - signer: NostrSigner, + fun build( + image: PictureMeta, + description: String, createdAt: Long = TimeUtils.now(), - onReady: (PictureEvent) -> Unit, - ) { - val image = - PictureMeta( - url, - mimeType, - blurhash, - dimensions, - alt, - hash, - size, - emptyList(), - emptyList(), - ) - - create(listOf(image), msg, title, usersMentioned, addressesMentioned, eventsMentioned, geohash, zapReceiver, markAsSensitive, zapRaiserAmount, signer, createdAt, onReady) + initializer: TagArrayBuilder.() -> Unit = {}, + ) = build(description, createdAt) { + pictureIMeta(image) + initializer() } - fun create( + fun build( images: List, - msg: String? = null, - title: String? = null, - usersMentioned: Set = emptySet(), - addressesMentioned: Set = emptySet(), - eventsMentioned: Set = emptySet(), - geohash: String? = null, - zapReceiver: List? = null, - markAsSensitive: Boolean = false, - zapRaiserAmount: Long? = null, - signer: NostrSigner, + description: String, createdAt: Long = TimeUtils.now(), - onReady: (PictureEvent) -> Unit, - ) { - val tags = mutableListOf(AltTagSerializer.toTagArray(ALT_DESCRIPTION)) + initializer: TagArrayBuilder.() -> Unit = {}, + ) = build(description, createdAt) { + pictureIMetas(images) + initializer() + } - images.forEach { - tags.add(it.toIMetaArray()) - } - - title?.let { tags.add(arrayOf("title", it)) } - - images.distinctBy { it.hash }.forEach { - if (it.hash != null) { - tags.add(arrayOf("x", it.hash)) - } - } - - images.distinctBy { it.mimeType }.forEach { - if (it.mimeType != null) { - tags.add(arrayOf("m", it.mimeType)) - } - } - - usersMentioned.forEach { tags.add(it.toPTagArray()) } - addressesMentioned.forEach { tags.add(it.toQTagArray()) } - eventsMentioned.forEach { tags.add(it.toQTagArray()) } - - if (msg != null) { - tags.addAll(buildHashtagTags(findHashtags(msg))) - tags.addAll(buildUrlRefs(findURLs(msg))) - } - zapReceiver?.forEach { tags.add(ZapSplitSetupSerializer.toTagArray(it)) } - zapRaiserAmount?.let { tags.add(ZapRaiserSerializer.toTagArray(it)) } - - if (markAsSensitive) { - tags.add(ContentWarningSerializer.toTagArray()) - } - - geohash?.let { tags.addAll(geohashMipMap(it)) } - - signer.sign(createdAt, KIND, tags.toTypedArray(), msg ?: "", onReady) - } - } -} - -class PictureMeta( - val url: String, - val mimeType: String?, - val blurhash: String?, - val dimension: Dimension?, - val alt: String?, - val hash: String?, - val size: Long?, - val fallback: List, - val annotations: List, -) { - fun toIMetaArray(): Array = - ( - listOfNotNull( - "imeta", - "$URL $url", - mimeType?.let { "$MIME_TYPE $it" }, - alt?.let { "$ALT $it" }, - hash?.let { "$HASH $it" }, - size?.let { "$FILE_SIZE $it" }, - dimension?.let { "$DIMENSION $it" }, - blurhash?.let { "$BLUR_HASH $it" }, - ) + - fallback.map { "$FALLBACK $it" } + - annotations.map { "$ANNOTATIONS $it" } - ).toTypedArray() - - companion object { - const val URL = "url" - const val MIME_TYPE = "m" - const val FILE_SIZE = "size" - const val DIMENSION = "dim" - const val HASH = "x" - const val BLUR_HASH = "blurhash" - const val ALT = "alt" - const val FALLBACK = "fallback" - const val ANNOTATIONS = "annotate-user" - - fun parse(tagArray: Array): PictureMeta? { - var url: String? = null - var mimeType: String? = null - var blurhash: String? = null - var dim: Dimension? = null - var alt: String? = null - var hash: String? = null - var size: Long? = null - val fallback = mutableListOf() - val annotations = mutableListOf() - - if (tagArray.size == 2 && - tagArray[1].contains(URL) && - ( - tagArray[1].contains(BLUR_HASH) || - tagArray[1].contains( - FILE_SIZE, - ) - ) - ) { - // hack to fix pablo's bug - val keys = setOf(URL, MIME_TYPE, BLUR_HASH, DIMENSION, ALT, HASH, FILE_SIZE, FALLBACK, ANNOTATIONS) - var keyNextValue: String? = null - val values = mutableListOf() - - tagArray[1].split(" ").forEach { - if (it in keys) { - if (keyNextValue != null && values.isNotEmpty()) { - when (keyNextValue) { - URL -> url = values.joinToString(" ") - MIME_TYPE -> mimeType = values.joinToString(" ") - BLUR_HASH -> blurhash = values.joinToString(" ") - DIMENSION -> dim = Dimension.parse(values.joinToString(" ")) - ALT -> alt = values.joinToString(" ") - HASH -> hash = values.joinToString(" ") - FILE_SIZE -> size = values.joinToString(" ").toLongOrNull() - FALLBACK -> fallback.add(values.joinToString(" ")) - ANNOTATIONS -> { - UserAnnotation.parse(values.joinToString(" "))?.let { - annotations.add(it) - } - } - } - values.clear() - } - keyNextValue = it - } else { - values.add(it) - } - } - - if (keyNextValue != null && values.isNotEmpty()) { - when (keyNextValue) { - URL -> url = values.joinToString(" ") - MIME_TYPE -> mimeType = values.joinToString(" ") - BLUR_HASH -> blurhash = values.joinToString(" ") - DIMENSION -> dim = Dimension.parse(values.joinToString(" ")) - ALT -> alt = values.joinToString(" ") - HASH -> hash = values.joinToString(" ") - FILE_SIZE -> size = values.joinToString(" ").toLongOrNull() - FALLBACK -> fallback.add(values.joinToString(" ")) - ANNOTATIONS -> { - UserAnnotation.parse(values.joinToString(" "))?.let { - annotations.add(it) - } - } - } - values.clear() - keyNextValue = null - } - } else { - tagArray.forEach { - val parts = it.split(" ", limit = 2) - val key = parts[0] - val value = if (parts.size == 2) parts[1] else "" - - if (value.isNotBlank()) { - when (key) { - URL -> url = value - MIME_TYPE -> mimeType = value - BLUR_HASH -> blurhash = value - DIMENSION -> dim = Dimension.parse(value) - ALT -> alt = value - HASH -> hash = value - FILE_SIZE -> size = value.toLongOrNull() - FALLBACK -> fallback.add(value) - ANNOTATIONS -> { - UserAnnotation.parse(value)?.let { - annotations.add(it) - } - } - } - } - } - } - - return url?.let { - PictureMeta(it, mimeType, blurhash, dim, alt, hash, size, fallback, annotations) - } - } - } -} - -class UserAnnotation( - val pubkey: HexKey, - val x: Int, - val y: Int, -) { - override fun toString() = "$pubkey:$x:$y" - - companion object { - fun parse(value: String): UserAnnotation? { - val ann = value.split(":") - if (ann.size == 3) { - val x = ann[1].toIntOrNull() - val y = ann[2].toIntOrNull() - if (x != null && y != null) { - return UserAnnotation(ann[0], x, y) - } - } - - return null + fun build( + description: String, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, description, createdAt) { + alt(ClassifiedsEvent.ALT_DESCRIPTION) + initializer() } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip68Picture/PictureMeta.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip68Picture/PictureMeta.kt new file mode 100644 index 000000000..df1ffab63 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip68Picture/PictureMeta.kt @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip68Picture + +import com.vitorpamplona.quartz.nip68Picture.tags.UserAnnotationTag +import com.vitorpamplona.quartz.nip92IMeta.IMetaTag +import com.vitorpamplona.quartz.nip92IMeta.IMetaTagBuilder +import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag + +data class PictureMeta( + val url: String, + val mimeType: String? = null, + val blurhash: String? = null, + val dimension: DimensionTag? = null, + val alt: String? = null, + val hash: String? = null, + val size: Int? = null, + val service: String? = null, + val fallback: List = emptyList(), + val annotations: List = emptyList(), +) { + fun toIMetaArray(): Array = + IMetaTagBuilder(url) + .apply { + mimeType?.let { mimeType(it) } + alt?.let { alt(it) } + hash?.let { hash(it) } + size?.let { size(it) } + dimension?.let { dims(it) } + blurhash?.let { blurhash(it) } + service?.let { service(it) } + fallback.forEach { fallback(it) } + annotations.forEach { userAnnotations(it) } + }.build() + .toTagArray() + + companion object { + fun parse(iMeta: IMetaTag): PictureMeta = + PictureMeta( + iMeta.url, + iMeta.mimeType()?.firstOrNull(), + iMeta.blurhash()?.firstOrNull(), + iMeta.dims()?.firstOrNull()?.let { DimensionTag.parse(it) }, + iMeta.alt()?.firstOrNull(), + iMeta.hash()?.firstOrNull(), + iMeta.size()?.firstOrNull()?.toIntOrNull(), + iMeta.service()?.firstOrNull(), + iMeta.fallback() ?: emptyList(), + iMeta.userAnnotations() ?: emptyList(), + ) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip68Picture/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip68Picture/TagArrayBuilderExt.kt new file mode 100644 index 000000000..4458d1649 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip68Picture/TagArrayBuilderExt.kt @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip68Picture + +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip23LongContent.tags.PublishedAtTag +import com.vitorpamplona.quartz.nip23LongContent.tags.TitleTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.HashTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.MimeTypeTag + +fun TagArrayBuilder.title(title: String) = addUnique(TitleTag.assemble(title)) + +fun TagArrayBuilder.summary(timestamp: Long) = addUnique(PublishedAtTag.assemble(timestamp)) + +fun TagArrayBuilder.pictureIMeta( + url: String, + mimeType: String? = null, + blurhash: String? = null, + dimension: DimensionTag? = null, + hash: String? = null, + size: Int? = null, + alt: String? = null, +) = pictureIMeta(PictureMeta(url, mimeType, blurhash, dimension, alt, hash, size)) + +fun TagArrayBuilder.pictureIMeta(imeta: PictureMeta): TagArrayBuilder { + add(imeta.toIMetaArray()) + imeta.hash?.let { add(HashTag.assemble(it)) } + imeta.mimeType?.let { add(MimeTypeTag.assemble(it)) } + return this +} + +fun TagArrayBuilder.pictureIMetas(imageUrls: List): TagArrayBuilder { + imageUrls.forEach { pictureIMeta(it) } + return this +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip68Picture/tags/UserAnnotationTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip68Picture/tags/UserAnnotationTag.kt new file mode 100644 index 000000000..c257825fa --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip68Picture/tags/UserAnnotationTag.kt @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip68Picture.tags + +import com.vitorpamplona.quartz.nip01Core.HexKey + +class UserAnnotationTag( + val pubkey: HexKey, + val x: Int, + val y: Int, +) { + override fun toString() = "$pubkey:$x:$y" + + companion object { + const val TAG_NAME = "annotate-user" + + @JvmStatic + fun parse(value: String): UserAnnotationTag? { + val ann = value.split(":") + if (ann.size == 3) { + val x = ann[1].toIntOrNull() + val y = ann[2].toIntOrNull() + if (x != null && y != null) { + return UserAnnotationTag(ann[0], x, y) + } + } + + return null + } + + @JvmStatic + fun assemble(tag: UserAnnotationTag) = arrayOf(TAG_NAME, tag.toString()) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/IMetaTagBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/IMetaTagBuilderExt.kt new file mode 100644 index 000000000..1822b8517 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/IMetaTagBuilderExt.kt @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip71Video + +import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip31Alts.AltTag +import com.vitorpamplona.quartz.nip36SensitiveContent.ContentWarningTag +import com.vitorpamplona.quartz.nip92IMeta.IMetaTagBuilder +import com.vitorpamplona.quartz.nip94FileMetadata.tags.BlurhashTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.FallbackTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.HashTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.ImageTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.MagnetTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.MimeTypeTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.OriginalHashTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.ServiceTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.SizeTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.SummaryTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.ThumbTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.TorrentInfoHash + +/** + * Contains the IMeta tags that are used by Video events. + */ +fun IMetaTagBuilder.magnet(uri: String) = add(MagnetTag.TAG_NAME, uri) + +fun IMetaTagBuilder.mimeType(mime: String) = add(MimeTypeTag.TAG_NAME, mime) + +fun IMetaTagBuilder.alt(alt: String) = add(AltTag.TAG_NAME, alt) + +fun IMetaTagBuilder.hash(hash: HexKey) = add(HashTag.TAG_NAME, hash) + +fun IMetaTagBuilder.size(size: Int) = add(SizeTag.TAG_NAME, size.toString()) + +fun IMetaTagBuilder.dims(dims: DimensionTag) = add(DimensionTag.TAG_NAME, dims.toString()) + +fun IMetaTagBuilder.blurhash(blurhash: String) = add(BlurhashTag.TAG_NAME, blurhash) + +fun IMetaTagBuilder.originalHash(originalHash: String) = add(OriginalHashTag.TAG_NAME, originalHash) + +fun IMetaTagBuilder.torrent(uri: String) = add(TorrentInfoHash.TAG_NAME, uri) + +fun IMetaTagBuilder.sensitiveContent(reason: String) = add(ContentWarningTag.TAG_NAME, reason) + +fun IMetaTagBuilder.image(imageUrl: HexKey) = add(ImageTag.TAG_NAME, imageUrl) + +fun IMetaTagBuilder.thumb(thumbUrl: HexKey) = add(ThumbTag.TAG_NAME, thumbUrl) + +fun IMetaTagBuilder.summary(summary: HexKey) = add(SummaryTag.TAG_NAME, summary) + +fun IMetaTagBuilder.fallback(fallback: HexKey) = add(FallbackTag.TAG_NAME, fallback) + +fun IMetaTagBuilder.service(service: HexKey) = add(ServiceTag.TAG_NAME, service) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/IMetaTagExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/IMetaTagExt.kt new file mode 100644 index 000000000..923d79029 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/IMetaTagExt.kt @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip71Video + +import com.vitorpamplona.quartz.nip31Alts.AltTag +import com.vitorpamplona.quartz.nip36SensitiveContent.ContentWarningTag +import com.vitorpamplona.quartz.nip92IMeta.IMetaTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.BlurhashTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.FallbackTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.HashTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.ImageTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.MagnetTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.MimeTypeTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.OriginalHashTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.ServiceTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.SizeTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.SummaryTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.ThumbTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.TorrentInfoHash + +/** + * Contains the IMeta tags that are used by Video events. + */ +fun IMetaTag.magnet() = properties.get(MagnetTag.TAG_NAME) + +fun IMetaTag.mimeType() = properties.get(MimeTypeTag.TAG_NAME) + +fun IMetaTag.alt() = properties.get(AltTag.TAG_NAME) + +fun IMetaTag.hash() = properties.get(HashTag.TAG_NAME) + +fun IMetaTag.size() = properties.get(SizeTag.TAG_NAME) + +fun IMetaTag.dims() = properties.get(DimensionTag.TAG_NAME) + +fun IMetaTag.blurhash() = properties.get(BlurhashTag.TAG_NAME) + +fun IMetaTag.originalHash() = properties.get(OriginalHashTag.TAG_NAME) + +fun IMetaTag.torrent() = properties.get(TorrentInfoHash.TAG_NAME) + +fun IMetaTag.sensitiveContent() = properties.get(ContentWarningTag.TAG_NAME) + +fun IMetaTag.image() = properties.get(ImageTag.TAG_NAME) + +fun IMetaTag.thumb() = properties.get(ThumbTag.TAG_NAME) + +fun IMetaTag.summary() = properties.get(SummaryTag.TAG_NAME) + +fun IMetaTag.fallback() = properties.get(FallbackTag.TAG_NAME) + +fun IMetaTag.service() = properties.get(ServiceTag.TAG_NAME) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/TagArrayBuilderExt.kt new file mode 100644 index 000000000..f64c18ec5 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/TagArrayBuilderExt.kt @@ -0,0 +1,62 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip71Video + +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.nip23LongContent.tags.PublishedAtTag +import com.vitorpamplona.quartz.nip23LongContent.tags.TitleTag +import com.vitorpamplona.quartz.nip71Video.tags.DurationTag +import com.vitorpamplona.quartz.nip71Video.tags.SegmentTag +import com.vitorpamplona.quartz.nip71Video.tags.TextTrackTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag + +fun TagArrayBuilder.title(title: String) = addUnique(TitleTag.assemble(title)) + +fun TagArrayBuilder.publishedAt(timestamp: Long) = addUnique(PublishedAtTag.assemble(timestamp)) + +fun TagArrayBuilder.duration(durationInSeconds: Int) = addUnique(DurationTag.assemble(durationInSeconds)) + +fun TagArrayBuilder.videoIMeta( + url: String, + mimeType: String? = null, + blurhash: String? = null, + dimension: DimensionTag? = null, + hash: String? = null, + size: Int? = null, + alt: String? = null, +) = videoIMeta(VideoMeta(url, mimeType, blurhash, dimension, alt, hash, size)) + +fun TagArrayBuilder.videoIMeta(imeta: VideoMeta) = add(imeta.toIMetaArray()) + +fun TagArrayBuilder.videoIMetas(imageUrls: List) = addAll(imageUrls.map { it.toIMetaArray() }) + +fun TagArrayBuilder.textTrack(track: TextTrackTag) = add(track.toTagArray()) + +fun TagArrayBuilder.textTracks(tracks: List) = addAll(tracks.map { it.toTagArray() }) + +fun TagArrayBuilder.segment(seg: SegmentTag) = add(seg.toTagArray()) + +fun TagArrayBuilder.segments(segs: List) = addAll(segs.map { it.toTagArray() }) + +fun TagArrayBuilder.participant(key: PTag) = add(key.toTagArray()) + +fun TagArrayBuilder.participants(keys: List) = addAll(keys.map { it.toTagArray() }) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/VideoEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/VideoEvent.kt index 0aea14adb..a17fd744c 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/VideoEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/VideoEvent.kt @@ -23,13 +23,29 @@ package com.vitorpamplona.quartz.nip71Video import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.core.any +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag +import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip22Comments.RootScope -import com.vitorpamplona.quartz.nip36SensitiveContent.ContentWarningSerializer -import com.vitorpamplona.quartz.nip92IMeta.Nip92MediaAttachments.Companion.IMETA -import com.vitorpamplona.quartz.nip94FileMetadata.Dimension -import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent -import com.vitorpamplona.quartz.utils.TimeUtils +import com.vitorpamplona.quartz.nip23LongContent.tags.PublishedAtTag +import com.vitorpamplona.quartz.nip23LongContent.tags.TitleTag +import com.vitorpamplona.quartz.nip31Alts.alt +import com.vitorpamplona.quartz.nip71Video.tags.DurationTag +import com.vitorpamplona.quartz.nip71Video.tags.SegmentTag +import com.vitorpamplona.quartz.nip92IMeta.imetas +import com.vitorpamplona.quartz.nip94FileMetadata.tags.BlurhashTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.FallbackTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.HashTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.ImageTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.MagnetTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.MimeTypeTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.ServiceTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.SizeTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.ThumbTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.TorrentInfoHash +import com.vitorpamplona.quartz.nip94FileMetadata.tags.UrlTag @Immutable abstract class VideoEvent( @@ -42,35 +58,38 @@ abstract class VideoEvent( sig: HexKey, ) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig), RootScope { - private fun url() = tags.firstOrNull { it.size > 1 && it[0] == URL }?.get(1) + /** old standard didnt use IMetas **/ + private fun url() = tags.firstNotNullOfOrNull(UrlTag::parse) - private fun urls() = tags.filter { it.size > 1 && it[0] == URL }.map { it[1] } + private fun urls() = tags.mapNotNull(UrlTag::parse) - private fun mimeType() = tags.firstOrNull { it.size > 1 && it[0] == MIME_TYPE }?.get(1) + private fun mimeType() = tags.firstNotNullOfOrNull(MimeTypeTag::parse) - private fun hash() = tags.firstOrNull { it.size > 1 && it[0] == HASH }?.get(1) + private fun hash() = tags.firstNotNullOfOrNull(HashTag::parse) - private fun size() = tags.firstOrNull { it.size > 1 && it[0] == FILE_SIZE }?.get(1) + private fun size() = tags.firstNotNullOfOrNull(SizeTag::parse) - private fun dimensions() = tags.firstOrNull { it.size > 1 && it[0] == DIMENSION }?.get(1)?.let { Dimension.parse(it) } + private fun dimensions() = tags.firstNotNullOfOrNull(DimensionTag::parse) - private fun blurhash() = tags.firstOrNull { it.size > 1 && it[0] == BLUR_HASH }?.get(1) + private fun magnetURI() = tags.firstNotNullOfOrNull(MagnetTag::parse) - private fun image() = tags.filter { it.size > 1 && it[0] == IMAGE }.map { it[1] } + private fun torrentInfoHash() = tags.firstNotNullOfOrNull(TorrentInfoHash::parse) - private fun thumb() = tags.firstOrNull { it.size > 1 && it[0] == THUMB }?.get(1) + private fun blurhash() = tags.firstNotNullOfOrNull(BlurhashTag::parse) - fun alt() = tags.firstOrNull { it.size > 1 && it[0] == ALT }?.get(1) + private fun hasUrl() = tags.any(UrlTag::isTag) - fun title() = tags.firstOrNull { it.size > 1 && it[0] == TITLE }?.get(1) + private fun isOneOf(mimeTypes: Set) = tags.any(MimeTypeTag::isIn, mimeTypes) - fun summary() = tags.firstOrNull { it.size > 1 && it[0] == SUMMARY }?.get(1) + private fun image() = tags.firstNotNullOfOrNull(ImageTag::parse) - fun duration() = tags.firstOrNull { it.size > 1 && it[0] == DURATION }?.get(1) + private fun images() = tags.mapNotNull(ImageTag::parse) - fun hasUrl() = tags.any { it.size > 1 && it[0] == URL } + private fun thumb() = tags.firstNotNullOfOrNull(ThumbTag::parse) - fun isOneOf(mimeTypes: Set) = tags.any { it.size > 1 && it[0] == FileHeaderEvent.MIME_TYPE && mimeTypes.contains(it[1]) } + private fun service() = tags.firstNotNullOfOrNull(ServiceTag::parse) + + private fun fallbacks() = tags.mapNotNull(FallbackTag::parse) // hack to fix pablo's bug fun rootVideo() = @@ -82,165 +101,30 @@ abstract class VideoEvent( alt = alt(), hash = hash(), dimension = dimensions(), - size = size()?.toIntOrNull(), - service = null, - fallback = emptyList(), - image = image(), + size = size(), + service = service(), + fallback = fallbacks(), + image = images().map { it.imageUrl }, ) } - fun imetaTags() = - tags - .map { tagArray -> - if (tagArray.size > 1 && tagArray[0] == IMETA) { - VideoMeta.parse(tagArray) - } else { - null - } - }.plus(rootVideo()) - .filterNotNull() + // --------------- + // current + // -------------- - companion object { - private const val URL = "url" - private const val MIME_TYPE = "m" - private const val FILE_SIZE = "size" - private const val DIMENSION = "dim" - private const val HASH = "x" - private const val BLUR_HASH = "blurhash" - private const val ALT = "alt" - private const val TITLE = "title" - private const val SUMMARY = "summary" - private const val DURATION = "duration" - private const val IMAGE = "image" - private const val THUMB = "thumb" + fun title() = tags.firstNotNullOfOrNull(TitleTag::parse) - fun create( - kind: Int, - dTag: String, - url: String, - mimeType: String? = null, - alt: String? = null, - hash: String? = null, - size: Int? = null, - duration: Int? = null, - dimensions: Dimension? = null, - blurhash: String? = null, - sensitiveContent: Boolean? = null, - service: String? = null, - altDescription: String, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (T) -> Unit, - ) { - val video = - VideoMeta( - url, - mimeType, - blurhash, - dimensions, - alt, - hash, - size, - service, - emptyList(), - emptyList(), - ) + fun publishedAt() = tags.firstNotNullOfOrNull(PublishedAtTag::parse) - val tags = mutableListOf>() + fun duration() = tags.firstNotNullOfOrNull(DurationTag::parse) - tags.add(arrayOf("d", dTag)) - tags.add(arrayOf(ALT, altDescription)) - if (sensitiveContent == true) { - tags.add(ContentWarningSerializer.toTagArray()) - } - duration?.let { tags.add(arrayOf(DURATION, "duration")) } + fun textTrack() = tags.mapNotNull(ETag::parse) - tags.add(video.toIMetaArray()) + fun segments() = tags.mapNotNull(SegmentTag::parse) - val content = alt ?: "" - signer.sign(createdAt, kind, tags.toTypedArray(), content, onReady) - } - } -} - -data class VideoMeta( - val url: String, - val mimeType: String?, - val blurhash: String?, - val dimension: Dimension?, - val alt: String?, - val hash: String?, - val size: Int?, - val service: String?, - val fallback: List, - val image: List, -) { - fun toIMetaArray(): Array = - ( - listOfNotNull( - "imeta", - "$URL $url", - mimeType?.let { "$MIME_TYPE $it" }, - alt?.let { "$ALT $it" }, - hash?.let { "$HASH $it" }, - size?.let { "$FILE_SIZE $it" }, - dimension?.let { "$DIMENSION $it" }, - blurhash?.let { "$BLUR_HASH $it" }, - service?.let { "$SERVICE $it" }, - ) + - fallback.map { "$FALLBACK $it" } + - image.map { "$IMAGE $it" } - - ).toTypedArray() - - companion object { - const val URL = "url" - const val MIME_TYPE = "m" - const val FILE_SIZE = "size" - const val DIMENSION = "dim" - const val HASH = "x" - const val BLUR_HASH = "blurhash" - const val ALT = "alt" - const val FALLBACK = "fallback" - const val IMAGE = "image" - const val SERVICE = "service" - - fun parse(tagArray: Array): VideoMeta? { - var url: String? = null - var mimeType: String? = null - var blurhash: String? = null - var dim: Dimension? = null - var alt: String? = null - var hash: String? = null - var size: Int? = null - var service: String? = null - val fallback = mutableListOf() - val images = mutableListOf() - - tagArray.forEach { - val parts = it.split(" ", limit = 2) - val key = parts[0] - val value = if (parts.size == 2) parts[1] else "" - - if (value.isNotBlank()) { - when (key) { - URL -> url = value - MIME_TYPE -> mimeType = value - BLUR_HASH -> blurhash = value - DIMENSION -> dim = Dimension.parse(value) - ALT -> alt = value - HASH -> hash = value - FILE_SIZE -> size = value.toIntOrNull() - SERVICE -> service = value - FALLBACK -> fallback.add(value) - IMAGE -> images.add(value) - } - } - } - - return url?.let { - VideoMeta(it, mimeType, blurhash, dim, alt, hash, size, service, fallback, images) - } - } - } + fun participants() = tags.mapNotNull(PTag::parse) + + fun hashtags() = tags.hashtags() + + fun imetaTags() = imetas().map { VideoMeta.parse(it) }.plus(rootVideo()).filterNotNull() } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/VideoHorizontalEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/VideoHorizontalEvent.kt index 3b26329b0..f39c53bc9 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/VideoHorizontalEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/VideoHorizontalEvent.kt @@ -22,9 +22,11 @@ package com.vitorpamplona.quartz.nip71Video import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.HexKey -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag import com.vitorpamplona.quartz.nip22Comments.RootScope -import com.vitorpamplona.quartz.nip94FileMetadata.Dimension +import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.utils.TimeUtils import java.util.UUID @@ -42,40 +44,37 @@ class VideoHorizontalEvent( const val KIND = 34235 const val ALT_DESCRIPTION = "Horizontal Video" - fun create( - url: String, - mimeType: String? = null, - alt: String? = null, - hash: String? = null, - size: Int? = null, - duration: Int? = null, - dimensions: Dimension? = null, - blurhash: String? = null, - sensitiveContent: Boolean? = null, - service: String? = null, + fun build( + video: VideoMeta, + description: String, dTag: String = UUID.randomUUID().toString(), - signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (VideoHorizontalEvent) -> Unit, - ) { - create( - kind = KIND, - dTag = dTag, - url = url, - mimeType = mimeType, - alt = alt, - hash = hash, - size = size, - duration = duration, - dimensions = dimensions, - blurhash = blurhash, - sensitiveContent = sensitiveContent, - service = service, - altDescription = ALT_DESCRIPTION, - signer = signer, - createdAt = createdAt, - onReady = onReady, - ) + initializer: TagArrayBuilder.() -> Unit = {}, + ) = build(description, dTag, createdAt) { + videoIMeta(video) + initializer() + } + + fun build( + video: List, + description: String, + dTag: String = UUID.randomUUID().toString(), + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = build(description, dTag, createdAt) { + videoIMetas(video) + initializer() + } + + fun build( + description: String, + dTag: String = UUID.randomUUID().toString(), + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, description, createdAt) { + dTag(dTag) + alt(ALT_DESCRIPTION) + initializer() } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/VideoMeta.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/VideoMeta.kt new file mode 100644 index 000000000..6d44e722c --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/VideoMeta.kt @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip71Video + +import com.vitorpamplona.quartz.nip92IMeta.IMetaTag +import com.vitorpamplona.quartz.nip92IMeta.IMetaTagBuilder +import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag + +data class VideoMeta( + val url: String, + val mimeType: String? = null, + val blurhash: String? = null, + val dimension: DimensionTag? = null, + val alt: String? = null, + val hash: String? = null, + val size: Int? = null, + val service: String? = null, + val fallback: List = emptyList(), + val image: List = emptyList(), +) { + fun toIMetaArray(): Array = + IMetaTagBuilder(url) + .apply { + mimeType?.let { mimeType(it) } + alt?.let { alt(it) } + hash?.let { hash(it) } + size?.let { size(it) } + dimension?.let { dims(it) } + blurhash?.let { blurhash(it) } + service?.let { service(it) } + fallback.forEach { fallback(it) } + image.forEach { image(it) } + }.build() + .toTagArray() + + companion object { + fun parse(iMeta: IMetaTag): VideoMeta = + VideoMeta( + iMeta.url, + iMeta.mimeType()?.firstOrNull(), + iMeta.blurhash()?.firstOrNull(), + iMeta.dims()?.firstOrNull()?.let { DimensionTag.parse(it) }, + iMeta.alt()?.firstOrNull(), + iMeta.hash()?.firstOrNull(), + iMeta.size()?.firstOrNull()?.toIntOrNull(), + iMeta.service()?.firstOrNull(), + iMeta.fallback() ?: emptyList(), + iMeta.image() ?: emptyList(), + ) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/VideoVerticalEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/VideoVerticalEvent.kt index 62c1771bd..58c808f49 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/VideoVerticalEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/VideoVerticalEvent.kt @@ -22,9 +22,11 @@ package com.vitorpamplona.quartz.nip71Video import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.HexKey -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag import com.vitorpamplona.quartz.nip22Comments.RootScope -import com.vitorpamplona.quartz.nip94FileMetadata.Dimension +import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.utils.TimeUtils import java.util.UUID @@ -42,40 +44,26 @@ class VideoVerticalEvent( const val KIND = 34236 const val ALT_DESCRIPTION = "Vertical Video" - fun create( - url: String, - mimeType: String? = null, - alt: String? = null, - hash: String? = null, - size: Int? = null, - duration: Int? = null, - dimensions: Dimension? = null, - blurhash: String? = null, - sensitiveContent: Boolean? = null, - service: String? = null, + fun build( + video: VideoMeta, + description: String, dTag: String = UUID.randomUUID().toString(), - signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (VideoVerticalEvent) -> Unit, - ) { - create( - kind = KIND, - dTag = dTag, - url = url, - mimeType = mimeType, - alt = alt, - hash = hash, - size = size, - duration = duration, - dimensions = dimensions, - blurhash = blurhash, - sensitiveContent = sensitiveContent, - service = service, - altDescription = ALT_DESCRIPTION, - signer = signer, - createdAt = createdAt, - onReady = onReady, - ) + initializer: TagArrayBuilder.() -> Unit = {}, + ) = build(description, dTag, createdAt) { + videoIMeta(video) + initializer() + } + + fun build( + description: String, + dTag: String = UUID.randomUUID().toString(), + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, description, createdAt) { + dTag(dTag) + alt(ALT_DESCRIPTION) + initializer() } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/VideoViewEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/VideoViewEvent.kt deleted file mode 100644 index 47ada41ef..000000000 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/VideoViewEvent.kt +++ /dev/null @@ -1,82 +0,0 @@ -/** - * Copyright (c) 2024 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.quartz.nip71Video - -import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey -import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag -import com.vitorpamplona.quartz.utils.TimeUtils - -@Immutable -class VideoViewEvent( - id: HexKey, - pubKey: HexKey, - createdAt: Long, - tags: Array>, - content: String, - sig: HexKey, -) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) { - companion object { - const val KIND = 34237 - - fun create( - video: ATag, - signer: NostrSigner, - viewStart: Long?, - viewEnd: Long?, - createdAt: Long = TimeUtils.now(), - onReady: (VideoViewEvent) -> Unit, - ) { - val tags = mutableListOf>() - - val aTag = video.toTag() - tags.add(arrayOf("d", aTag)) - tags.add(arrayOf("a", aTag)) - if (viewEnd != null) { - tags.add(arrayOf("viewed", viewStart?.toString() ?: "0", viewEnd.toString())) - } else { - tags.add(arrayOf("viewed", viewStart?.toString() ?: "0")) - } - - signer.sign(createdAt, KIND, tags.toTypedArray(), "", onReady) - } - - fun addViewedTime( - event: VideoViewEvent, - viewStart: Long?, - viewEnd: Long?, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (VideoViewEvent) -> Unit, - ) { - val tags = event.tags.toMutableList() - if (viewEnd != null) { - tags.add(arrayOf("viewed", viewStart?.toString() ?: "0", viewEnd.toString())) - } else { - tags.add(arrayOf("viewed", viewStart?.toString() ?: "0")) - } - - signer.sign(createdAt, KIND, tags.toTypedArray(), "", onReady) - } - } -} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/tags/DurationTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/tags/DurationTag.kt new file mode 100644 index 000000000..4e4fed010 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/tags/DurationTag.kt @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip71Video.tags + +class DurationTag { + companion object { + const val TAG_NAME = "duration" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): Int? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1].toIntOrNull() + } + + @JvmStatic + fun assemble(durationInSeconds: Int) = arrayOf(TAG_NAME, durationInSeconds.toString()) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/tags/SegmentTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/tags/SegmentTag.kt new file mode 100644 index 000000000..6feb0fea0 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/tags/SegmentTag.kt @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip71Video.tags + +import com.vitorpamplona.quartz.utils.arrayOfNotNull + +class SegmentTag( + val start: String, // HH:MM:SS.sss + val end: String, // HH:MM:SS.sss + val title: String, + val thumbnailUrl: String?, +) { + fun toTagArray() = assemble(start, end, title, thumbnailUrl) + + companion object { + const val TAG_NAME = "segment" + const val TAG_SIZE = 4 + + @JvmStatic + fun parse(tag: Array): SegmentTag? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return SegmentTag(tag[1], tag[2], tag[3], tag.getOrNull(4)) + } + + @JvmStatic + fun assemble( + start: String, // HH:MM:SS.sss + end: String, // HH:MM:SS.sss + title: String, + thumbnailUrl: String?, + ) = arrayOfNotNull(TAG_NAME, start, end, title, thumbnailUrl) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/PTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/tags/TextTrackTag.kt similarity index 57% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/PTag.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/tags/TextTrackTag.kt index 06b4c2f5d..645949e74 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/PTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/tags/TextTrackTag.kt @@ -18,42 +18,38 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip10Notes +package com.vitorpamplona.quartz.nip71Video.tags -import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.HexKey -import com.vitorpamplona.quartz.nip01Core.hexToByteArray -import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile -import com.vitorpamplona.quartz.nip19Bech32.toNpub +import com.vitorpamplona.quartz.utils.arrayOfNotNull import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.pointerSizeInBytes -import com.vitorpamplona.quartz.utils.removeTrailingNullsAndEmptyOthers -@Immutable -data class PTag( - val pubKeyHex: HexKey, +data class TextTrackTag( + val eventId: HexKey, + var relay: String? = null, ) { - var relay: String? = null - - constructor(pubKeyHex: HexKey, relayHint: String?) : this(pubKeyHex) { - this.relay = relayHint?.ifBlank { null } - } - fun countMemory(): Long = - 2 * pointerSizeInBytes + // 2 fields, 4 bytes each reference (32bit) - pubKeyHex.bytesUsedInMemory() + + 2 * pointerSizeInBytes + // 3 fields, 4 bytes each reference (32bit) + eventId.bytesUsedInMemory() + (relay?.bytesUsedInMemory() ?: 0) - fun toNProfile(): String = NProfile.create(pubKeyHex, relay?.let { listOf(it) } ?: emptyList()) - - fun toNPub(): String = pubKeyHex.hexToByteArray().toNpub() - - fun toPTagArray() = removeTrailingNullsAndEmptyOthers("p", pubKeyHex, relay) + fun toTagArray() = arrayOfNotNull(TAG_NAME, eventId, relay) companion object { - fun parse(tags: Array): PTag { - require(tags[0] == "p") - return PTag(tags[1], tags.getOrNull(2)) + const val TAG_NAME = "text-track" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): TextTrackTag? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return TextTrackTag(tag[1], tag.getOrNull(2)) } + + @JvmStatic + fun assemble( + eventId: HexKey, + relay: String?, + ) = arrayOfNotNull(TAG_NAME, eventId, relay) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/CommunityListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/CommunityListEvent.kt index 2e8c7a3fd..afdf59ba3 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/CommunityListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/CommunityListEvent.kt @@ -26,7 +26,7 @@ import com.vitorpamplona.quartz.nip01Core.core.BaseReplaceableEvent.Companion.FI import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip19Bech32.parseAtag -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer +import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.nip51Lists.GeneralListEvent import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.pointerSizeInBytes @@ -270,7 +270,7 @@ class CommunityListEvent( if (tags.any { it.size > 1 && it[0] == "alt" }) { tags } else { - tags + AltTagSerializer.toTagArray(ALT) + tags + AltTag.assemble(ALT) } signer.sign(createdAt, KIND, newTags, content, onReady) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/CommunityPostApprovalEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/approval/CommunityPostApprovalEvent.kt similarity index 53% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/CommunityPostApprovalEvent.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/approval/CommunityPostApprovalEvent.kt index 80e4dc3a4..3ff83eaba 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/CommunityPostApprovalEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/approval/CommunityPostApprovalEvent.kt @@ -18,16 +18,20 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip72ModCommunities +package com.vitorpamplona.quartz.nip72ModCommunities.approval import android.util.Log import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.EventHintBundle import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag -import com.vitorpamplona.quartz.nip19Bech32.parse -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedAddresses +import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEvents +import com.vitorpamplona.quartz.nip01Core.tags.kinds.kind +import com.vitorpamplona.quartz.nip31Alts.alt +import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent import com.vitorpamplona.quartz.utils.TimeUtils @Immutable @@ -50,52 +54,30 @@ class CommunityPostApprovalEvent( null } - fun communities() = - tags - .filter { it.size > 1 && it[0] == "a" } - .mapNotNull { - val aTag = ATag.parse(it[1], it.getOrNull(2)) + fun communities() = taggedAddresses().filter { it.kind == CommunityDefinitionEvent.KIND } - if (aTag?.kind == CommunityDefinitionEvent.KIND) { - aTag - } else { - null - } - } + fun approvedEvents() = taggedEvents() - fun approvedEvents() = - tags - .filter { - it.size > 1 && - ( - it[0] == "e" || - (it[0] == "a" && ATag.parse(it[1], null)?.kind != CommunityDefinitionEvent.KIND) - ) - }.map { it[1] } + fun approvedAddresses() = taggedAddresses().filter { it.kind != CommunityDefinitionEvent.KIND } companion object { const val KIND = 4550 - const val ALT = "Community post approval" + const val ALT_DESCRIPTION = "Community post approval" - fun create( - approvedPost: Event, - community: CommunityDefinitionEvent, - signer: NostrSigner, + fun build( + approvedPost: EventHintBundle, + community: EventHintBundle, createdAt: Long = TimeUtils.now(), - onReady: (CommunityPostApprovalEvent) -> Unit, - ) { - val content = approvedPost.toJson() + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, "", createdAt) { + alt(ALT_DESCRIPTION) - val communities = arrayOf("a", community.address().toTag()) - val replyToPost = arrayOf("e", approvedPost.id) - val replyToAuthor = arrayOf("p", approvedPost.pubKey) - val innerKind = arrayOf("k", "${approvedPost.kind}") - val alt = AltTagSerializer.toTagArray(ALT) + community(community) + approved(approvedPost) + notifyAuthor(approvedPost) + kind(approvedPost.event.kind) - val tags: Array> = - arrayOf(communities, replyToPost, replyToAuthor, innerKind, alt) - - signer.sign(createdAt, KIND, tags, content, onReady) + initializer() } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/approval/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/approval/TagArrayBuilderExt.kt new file mode 100644 index 000000000..7b8e26e1d --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/approval/TagArrayBuilderExt.kt @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip72ModCommunities.approval + +import com.vitorpamplona.quartz.nip01Core.EventHintBundle +import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent + +fun TagArrayBuilder.community(event: EventHintBundle) = add(event.toATag().toATagArray()) + +fun TagArrayBuilder.approved(event: EventHintBundle) { + add(event.toETagArray()) + if (event.event is AddressableEvent) { + add(event.toATag().toATagArray()) + } +} + +fun TagArrayBuilder.notifyAuthor(event: EventHintBundle) { + add(event.toETagArray()) +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/definition/CommunityDefinitionEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/definition/CommunityDefinitionEvent.kt new file mode 100644 index 000000000..bb57e6822 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/definition/CommunityDefinitionEvent.kt @@ -0,0 +1,91 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip72ModCommunities.definition + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag +import com.vitorpamplona.quartz.nip31Alts.alt +import com.vitorpamplona.quartz.nip72ModCommunities.definition.tags.DescriptionTag +import com.vitorpamplona.quartz.nip72ModCommunities.definition.tags.ImageTag +import com.vitorpamplona.quartz.nip72ModCommunities.definition.tags.ModeratorTag +import com.vitorpamplona.quartz.nip72ModCommunities.definition.tags.NameTag +import com.vitorpamplona.quartz.nip72ModCommunities.definition.tags.RelayTag +import com.vitorpamplona.quartz.nip72ModCommunities.definition.tags.RulesTag +import com.vitorpamplona.quartz.utils.TimeUtils +import java.util.UUID + +@Immutable +class CommunityDefinitionEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) { + fun name() = tags.firstNotNullOfOrNull(NameTag::parse) + + fun description() = tags.firstNotNullOfOrNull(DescriptionTag::parse) + + fun image() = tags.firstNotNullOfOrNull(ImageTag::parse) + + fun rules() = tags.firstNotNullOfOrNull(RulesTag::parse) + + fun moderators() = tags.mapNotNull(ModeratorTag::parse) + + fun moderatorKeys() = tags.mapNotNull(ModeratorTag::parseKey) + + fun relays() = tags.mapNotNull(RelayTag::parse) + + companion object { + const val KIND = 34550 + const val ALT_DESCRIPTION = "Community definition" + + fun build( + name: String, + description: String, + moderators: List, + image: String? = null, + rules: String? = null, + relays: List? = null, + dTag: String = UUID.randomUUID().toString(), + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, "", createdAt) { + alt(ALT_DESCRIPTION) + + dTag(dTag) + name(name) + description(description) + moderators(moderators) + + relays?.let { relays(it) } + rules?.let { rules(it) } + image?.let { image(image) } + + initializer() + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/definition/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/definition/TagArrayBuilderExt.kt new file mode 100644 index 000000000..e0aa6e2c3 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/definition/TagArrayBuilderExt.kt @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip72ModCommunities.definition + +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip23LongContent.tags.ImageTag +import com.vitorpamplona.quartz.nip72ModCommunities.definition.tags.DescriptionTag +import com.vitorpamplona.quartz.nip72ModCommunities.definition.tags.ModeratorTag +import com.vitorpamplona.quartz.nip72ModCommunities.definition.tags.NameTag +import com.vitorpamplona.quartz.nip72ModCommunities.definition.tags.RelayTag +import com.vitorpamplona.quartz.nip72ModCommunities.definition.tags.RulesTag + +fun TagArrayBuilder.name(name: String) = addUnique(NameTag.assemble(name)) + +fun TagArrayBuilder.description(description: String) = addUnique(DescriptionTag.assemble(description)) + +fun TagArrayBuilder.image(webUrl: String) = addUnique(ImageTag.assemble(webUrl)) + +fun TagArrayBuilder.rules(rules: String) = addUnique(RulesTag.assemble(rules)) + +fun TagArrayBuilder.moderators(mods: List) = addAll(mods.map { it.toTagArray() }) + +fun TagArrayBuilder.relays(relays: List) = addAll(relays.map { it.toTagArray() }) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/definition/tags/DescriptionTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/definition/tags/DescriptionTag.kt new file mode 100644 index 000000000..84a2edb9d --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/definition/tags/DescriptionTag.kt @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip72ModCommunities.definition.tags + +class DescriptionTag { + companion object { + const val TAG_NAME = "description" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): String? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1] + } + + @JvmStatic + fun assemble(title: String) = arrayOf(TAG_NAME, title) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/definition/tags/ImageTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/definition/tags/ImageTag.kt new file mode 100644 index 000000000..9bae1d46a --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/definition/tags/ImageTag.kt @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip72ModCommunities.definition.tags + +import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag +import com.vitorpamplona.quartz.utils.arrayOfNotNull + +class ImageTag( + val imageUrl: String, + val dimensions: DimensionTag? = null, +) { + companion object { + const val TAG_NAME = "image" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): ImageTag? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + if (tag[1].isEmpty()) return null + val dims = tag.getOrNull(2)?.let { DimensionTag.parse(it) } + return ImageTag(tag[1], dims) + } + + @JvmStatic + fun assemble( + imageUrl: String, + dimensions: DimensionTag? = null, + ) = arrayOfNotNull(TAG_NAME, imageUrl, dimensions?.toString()) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/definition/tags/ModeratorTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/definition/tags/ModeratorTag.kt new file mode 100644 index 000000000..f6c6db6b0 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/definition/tags/ModeratorTag.kt @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip72ModCommunities.definition.tags + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.Tag +import com.vitorpamplona.quartz.nip01Core.core.isNotName +import com.vitorpamplona.quartz.nip01Core.tags.people.PubKeyReferenceTag +import com.vitorpamplona.quartz.utils.arrayOfNotNull + +@Immutable +data class ModeratorTag( + override val pubKey: String, + override val relayHint: String?, + val role: String?, +) : PubKeyReferenceTag { + fun toTagArray() = assemble(pubKey, relayHint, role) + + companion object { + const val TAG_NAME = "p" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Tag): ModeratorTag? { + if (tag.isNotName(TAG_NAME, TAG_SIZE)) return null + if (tag[1].length != 64) return null + return ModeratorTag(tag[1], tag.getOrNull(2), tag.getOrNull(3)) + } + + @JvmStatic + fun parseKey(tag: Tag): String? { + if (tag.isNotName(TAG_NAME, TAG_SIZE)) return null + if (tag[1].length != 64) return null + return tag[1] + } + + @JvmStatic + fun assemble( + pubkey: HexKey, + relayHint: String?, + role: String?, + ) = arrayOfNotNull(TAG_NAME, pubkey, relayHint, role) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/definition/tags/NameTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/definition/tags/NameTag.kt new file mode 100644 index 000000000..0c66da66d --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/definition/tags/NameTag.kt @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip72ModCommunities.definition.tags + +class NameTag { + companion object { + const val TAG_NAME = "name" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): String? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1] + } + + @JvmStatic + fun assemble(name: String) = arrayOf(TAG_NAME, name) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/definition/tags/RelayTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/definition/tags/RelayTag.kt new file mode 100644 index 000000000..a70f2f7d9 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/definition/tags/RelayTag.kt @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip72ModCommunities.definition.tags + +import com.vitorpamplona.quartz.utils.arrayOfNotNull + +class RelayTag( + val url: String, + val marker: String? = null, +) { + fun toTagArray() = assemble(url, marker) + + companion object { + const val TAG_NAME = "relay" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): RelayTag? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return RelayTag(tag[1], tag.getOrNull(2)) + } + + @JvmStatic + fun assemble( + url: String, + marker: String? = null, + ) = arrayOfNotNull(TAG_NAME, url, marker) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/definition/tags/RulesTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/definition/tags/RulesTag.kt new file mode 100644 index 000000000..467216f8f --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/definition/tags/RulesTag.kt @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip72ModCommunities.definition.tags + +class RulesTag { + companion object { + const val TAG_NAME = "rules" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): String? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1] + } + + @JvmStatic + fun assemble(rules: String) = arrayOf(TAG_NAME, rules) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip75ZapGoals/GoalEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip75ZapGoals/GoalEvent.kt index 49231c6ff..a8cb100a5 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip75ZapGoals/GoalEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip75ZapGoals/GoalEvent.kt @@ -21,12 +21,21 @@ package com.vitorpamplona.quartz.nip75ZapGoals import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.EventHintBundle import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags +import com.vitorpamplona.quartz.nip01Core.tags.references.reference +import com.vitorpamplona.quartz.nip23LongContent.tags.ImageTag +import com.vitorpamplona.quartz.nip23LongContent.tags.SummaryTag +import com.vitorpamplona.quartz.nip31Alts.alt +import com.vitorpamplona.quartz.nip75ZapGoals.tags.AmountTag +import com.vitorpamplona.quartz.nip75ZapGoals.tags.ClosedAtTag +import com.vitorpamplona.quartz.nip75ZapGoals.tags.RelayListTag import com.vitorpamplona.quartz.utils.TimeUtils @Immutable @@ -38,47 +47,48 @@ class GoalEvent( content: String, sig: HexKey, ) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) { + fun topics() = hashtags() + + fun image() = tags.firstNotNullOfOrNull(ImageTag::parse) + + fun summary() = tags.firstNotNullOfOrNull(SummaryTag::parse) + + fun closedAt() = tags.firstNotNullOfOrNull(ClosedAtTag::parse) + + fun amount() = tags.firstNotNullOfOrNull(AmountTag::parse) + + fun relays() = tags.firstNotNullOfOrNull(RelayListTag::parse) + companion object { const val KIND = 9041 - const val ALT = "Zap Goal" + const val ALT_DESCRIPTION = "Zap Goal" - private const val SUMMARY = "summary" - private const val CLOSED_AT = "closed_at" - private const val IMAGE = "image" - private const val AMOUNT = "amount" - - fun create( + fun build( description: String, amount: Long, - relays: Set, + relays: List, closedAt: Long? = null, image: String? = null, summary: String? = null, websiteUrl: String? = null, - linkedEvent: Event? = null, - signer: NostrSigner, + linkedEvent: EventHintBundle? = null, createdAt: Long = TimeUtils.now(), - onReady: (GoalEvent) -> Unit, - ) { - val tags = - mutableListOf( - arrayOf(AMOUNT, amount.toString()), - arrayOf("relays") + relays, - AltTagSerializer.toTagArray(ALT), - ) - - if (linkedEvent is AddressableEvent) { - tags.add(arrayOf("a", linkedEvent.address().toTag())) - } else if (linkedEvent is Event) { - tags.add(arrayOf("e", linkedEvent.id)) + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, description, createdAt) { + amount(amount) + relays(relays) + closedAt?.let { closedAt(it) } + image?.let { image(it) } + summary?.let { summary(it) } + websiteUrl?.let { reference(it) } + linkedEvent?.let { + if (it.event is AddressableEvent) { + linked(it.toATag()) + } + linked(it.toETag()) } - - closedAt?.let { tags.add(arrayOf(CLOSED_AT, it.toString())) } - summary?.let { tags.add(arrayOf(SUMMARY, it)) } - image?.let { tags.add(arrayOf(IMAGE, it)) } - websiteUrl?.let { tags.add(arrayOf("r", it)) } - - signer.sign(createdAt, KIND, tags.toTypedArray(), description, onReady) + alt(ALT_DESCRIPTION) + initializer() } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip75ZapGoals/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip75ZapGoals/TagArrayBuilderExt.kt new file mode 100644 index 000000000..8cb28f0d3 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip75ZapGoals/TagArrayBuilderExt.kt @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip75ZapGoals + +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag +import com.vitorpamplona.quartz.nip23LongContent.tags.ImageTag +import com.vitorpamplona.quartz.nip23LongContent.tags.SummaryTag +import com.vitorpamplona.quartz.nip75ZapGoals.tags.AmountTag +import com.vitorpamplona.quartz.nip75ZapGoals.tags.ClosedAtTag +import com.vitorpamplona.quartz.nip75ZapGoals.tags.RelayListTag + +fun TagArrayBuilder.amount(amountInMillisats: Long) = addUnique(AmountTag.assemble(amountInMillisats)) + +fun TagArrayBuilder.relays(urls: List) = add(RelayListTag.assemble(urls)) + +fun TagArrayBuilder.summary(summary: String) = addUnique(SummaryTag.assemble(summary)) + +fun TagArrayBuilder.image(imageUrl: String) = addUnique(ImageTag.assemble(imageUrl)) + +fun TagArrayBuilder.closedAt(closedAt: Long) = addUnique(ClosedAtTag.assemble(closedAt)) + +fun TagArrayBuilder.linked(tag: ETag) = addUnique(tag.toTagArray()) + +fun TagArrayBuilder.linked(tag: ATag) = addUnique(tag.toATagArray()) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip75ZapGoals/tags/AmountTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip75ZapGoals/tags/AmountTag.kt new file mode 100644 index 000000000..e5c24cae3 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip75ZapGoals/tags/AmountTag.kt @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip75ZapGoals.tags + +class AmountTag { + companion object { + const val TAG_NAME = "amount" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): Long? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1].toLongOrNull() + } + + @JvmStatic + fun assemble(amountInMillisats: Long) = arrayOf(TAG_NAME, amountInMillisats.toString()) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip75ZapGoals/tags/ClosedAtTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip75ZapGoals/tags/ClosedAtTag.kt new file mode 100644 index 000000000..2fbd2a885 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip75ZapGoals/tags/ClosedAtTag.kt @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip75ZapGoals.tags + +class ClosedAtTag { + companion object { + const val TAG_NAME = "closed_at" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): Long? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1].toLongOrNull() + } + + @JvmStatic + fun assemble(timestamp: Long) = arrayOf(TAG_NAME, timestamp.toString()) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip75ZapGoals/tags/RelayListTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip75ZapGoals/tags/RelayListTag.kt new file mode 100644 index 000000000..bb5c0811a --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip75ZapGoals/tags/RelayListTag.kt @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip75ZapGoals.tags + +class RelayListTag( + val relayUrls: List, +) { + companion object { + const val TAG_NAME = "relays" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): RelayListTag? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + val relays = + tag.mapIndexedNotNull { index, s -> + if (index == 0) null else s + } + return RelayListTag(relays) + } + + @JvmStatic + fun assemble(urls: List) = arrayOf(TAG_NAME) + urls.toTypedArray() + + @JvmStatic + fun assemble(tag: RelayListTag) = assemble(tag.relayUrls) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip78AppData/AppSpecificDataEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip78AppData/AppSpecificDataEvent.kt index 2b2f40cb9..b030fb84e 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip78AppData/AppSpecificDataEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip78AppData/AppSpecificDataEvent.kt @@ -24,7 +24,7 @@ import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer +import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.utils.TimeUtils class AppSpecificDataEvent( @@ -61,7 +61,7 @@ class AppSpecificDataEvent( val newTags = if (withD.none { it.size > 0 && it[0] == "alt" }) { - withD + AltTagSerializer.toTagArray(ALT) + withD + AltTag.assemble(ALT) } else { withD } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip84Highlights/HighlightEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip84Highlights/HighlightEvent.kt index 84475ac03..6b50b3c81 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip84Highlights/HighlightEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip84Highlights/HighlightEvent.kt @@ -22,13 +22,14 @@ package com.vitorpamplona.quartz.nip84Highlights import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.HexKey -import com.vitorpamplona.quartz.nip01Core.core.firstTagValue import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.addressables.firstTaggedAddress import com.vitorpamplona.quartz.nip01Core.tags.events.firstTaggedEvent import com.vitorpamplona.quartz.nip01Core.tags.people.firstTaggedUser -import com.vitorpamplona.quartz.nip10Notes.BaseTextNoteEvent -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer +import com.vitorpamplona.quartz.nip01Core.tags.references.ReferenceTag +import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent +import com.vitorpamplona.quartz.nip31Alts.AltTag +import com.vitorpamplona.quartz.nip84Highlights.tags.ContextTag import com.vitorpamplona.quartz.utils.TimeUtils @Immutable @@ -39,14 +40,14 @@ class HighlightEvent( tags: Array>, content: String, sig: HexKey, -) : BaseTextNoteEvent(id, pubKey, createdAt, KIND, tags, content, sig) { - fun inUrl() = tags.firstTagValue("r") +) : BaseThreadedEvent(id, pubKey, createdAt, KIND, tags, content, sig) { + fun inUrl() = tags.firstNotNullOfOrNull(ReferenceTag::parse) fun author() = firstTaggedUser() fun quote() = content - fun context() = tags.firstTagValue("context") + fun context() = tags.firstNotNullOfOrNull(ContextTag::parse) fun inPost() = firstTaggedAddress() @@ -62,7 +63,7 @@ class HighlightEvent( createdAt: Long = TimeUtils.now(), onReady: (HighlightEvent) -> Unit, ) { - signer.sign(createdAt, KIND, arrayOf(AltTagSerializer.toTagArray(ALT)), msg, onReady) + signer.sign(createdAt, KIND, arrayOf(AltTag.assemble(ALT)), msg, onReady) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip84Highlights/tags/ContextTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip84Highlights/tags/ContextTag.kt new file mode 100644 index 000000000..6e46a32e7 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip84Highlights/tags/ContextTag.kt @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip84Highlights.tags + +class ContextTag { + companion object { + const val TAG_NAME = "context" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): String? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1] + } + + @JvmStatic + fun assemble(context: String) = arrayOf(TAG_NAME, context) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/PlatformType.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/PlatformType.kt new file mode 100644 index 000000000..567018380 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/PlatformType.kt @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip89AppHandlers + +enum class PlatformType( + val code: String, +) { + WEB("web"), + IOS("ios"), + ANDROID("android"), +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/AppDefinitionEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/definition/AppDefinitionEvent.kt similarity index 57% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/AppDefinitionEvent.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/definition/AppDefinitionEvent.kt index e27e09ca9..bc365793e 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/AppDefinitionEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/definition/AppDefinitionEvent.kt @@ -18,19 +18,24 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip89AppHandlers +package com.vitorpamplona.quartz.nip89AppHandlers.definition import android.util.Log import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.HexKey -import com.vitorpamplona.quartz.nip01Core.UserMetadata import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent -import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag +import com.vitorpamplona.quartz.nip01Core.tags.kinds.isTaggedKind +import com.vitorpamplona.quartz.nip01Core.tags.kinds.kinds import com.vitorpamplona.quartz.nip21UriScheme.toNostrUri -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer +import com.vitorpamplona.quartz.nip23LongContent.tags.PublishedAtTag +import com.vitorpamplona.quartz.nip31Alts.alt +import com.vitorpamplona.quartz.nip89AppHandlers.PlatformType +import com.vitorpamplona.quartz.nip89AppHandlers.definition.tags.PlatformLinkTag import com.vitorpamplona.quartz.utils.TimeUtils -import java.io.ByteArrayInputStream +import java.util.UUID @Immutable class AppDefinitionEvent( @@ -50,14 +55,8 @@ class AppDefinitionEvent( cachedMetadata } else { try { - val newMetadata = - EventMapper.mapper.readValue( - ByteArrayInputStream(content.toByteArray(Charsets.UTF_8)), - AppMetadata::class.java, - ) - + val newMetadata = AppMetadata.parse(content) cachedMetadata = newMetadata - newMetadata } catch (e: Exception) { e.printStackTrace() @@ -66,29 +65,40 @@ class AppDefinitionEvent( } } - fun supportedKinds() = - tags - .filter { it.size > 1 && it[0] == "k" } - .mapNotNull { runCatching { it[1].toInt() }.getOrNull() } + fun supportedKinds() = tags.kinds() - fun includeKind(kind: String) = tags.any { it.size > 1 && it[0] == "k" && it[1] == kind } + fun includeKind(kind: Int) = tags.isTaggedKind(kind) - fun publishedAt() = tags.firstOrNull { it.size > 1 && it[0] == "published_at" }?.get(1) + fun publishedAt() = tags.firstNotNullOfOrNull(PublishedAtTag::parse) companion object { const val KIND = 31990 + const val ALT_DESCRIPTION = "App definition event" - fun create( - details: UserMetadata, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (AppDefinitionEvent) -> Unit, + // ["web", "https://..../a/", "nevent"] + class PlatformLink( + val platform: PlatformType, + val uri: String, + val entityType: EntityType?, ) { - val tags = - arrayOf( - AltTagSerializer.toTagArray("App definition event for ${details.name}"), - ) - signer.sign(createdAt, KIND, tags, "", onReady) + fun toPlatformLinkTag() = PlatformLinkTag(platform.code, uri, entityType?.code) + + fun toTagArray() = toPlatformLinkTag().toTagArray() + } + + fun build( + details: AppMetadata, + supportedKinds: Set, + links: List, + dTag: String = UUID.randomUUID().toString(), + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, details.toJson(), createdAt) { + dTag(dTag) + alt(ALT_DESCRIPTION) + kinds(supportedKinds) + links(links) + initializer() } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/AppMetadata.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/definition/AppMetadata.kt similarity index 91% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/AppMetadata.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/definition/AppMetadata.kt index d57d5825e..af2362174 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/AppMetadata.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/definition/AppMetadata.kt @@ -18,11 +18,11 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip89AppHandlers +package com.vitorpamplona.quartz.nip89AppHandlers.definition import androidx.compose.runtime.Stable import com.fasterxml.jackson.annotation.JsonProperty -import com.vitorpamplona.quartz.nip02FollowList.ImmutableListOfLists +import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.pointerSizeInBytes @@ -50,11 +50,6 @@ class AppMetadata { var lud06: String? = null var lud16: String? = null - var twitter: String? = null - - @Transient - var tags: ImmutableListOfLists? = null - fun countMemory(): Long = 20 * pointerSizeInBytes + // 20 fields, 4 bytes for each reference (name?.bytesUsedInMemory() ?: 0L) + @@ -73,9 +68,7 @@ class AppMetadata { (nip05?.bytesUsedInMemory() ?: 0L) + (domain?.bytesUsedInMemory() ?: 0L) + (lud06?.bytesUsedInMemory() ?: 0L) + - (lud16?.bytesUsedInMemory() ?: 0L) + - (twitter?.bytesUsedInMemory() ?: 0L) + - (tags?.lists?.sumOf { it.sumOf { it.bytesUsedInMemory() } } ?: 0L) + (lud16?.bytesUsedInMemory() ?: 0L) fun anyName(): String? = displayName ?: name ?: username @@ -116,4 +109,12 @@ class AppMetadata { if (website?.isBlank() == true) website = null if (domain?.isBlank() == true) domain = null } + + fun toJson() = assemble(this) + + companion object { + fun assemble(data: AppMetadata) = EventMapper.mapper.writeValueAsString(data) + + fun parse(content: String) = EventMapper.mapper.readValue(content, AppMetadata::class.java) + } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/definition/EntityType.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/definition/EntityType.kt new file mode 100644 index 000000000..d3ae3cfb8 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/definition/EntityType.kt @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip89AppHandlers.definition + +enum class EntityType( + val code: String, +) { + NOTE("note"), + NEVENT("nevent"), + NADDR("naddr"), + NPUB("npub"), + NPROFILE("nprofile"), +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/definition/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/definition/TagArrayBuilderExt.kt new file mode 100644 index 000000000..d5b2d20db --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/definition/TagArrayBuilderExt.kt @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip89AppHandlers.definition + +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent.Companion.PlatformLink + +fun TagArrayBuilder.links(links: List) = addAll(links.map { it.toTagArray() }) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/definition/tags/PlatformLinkTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/definition/tags/PlatformLinkTag.kt new file mode 100644 index 000000000..94f5f798d --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/definition/tags/PlatformLinkTag.kt @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip89AppHandlers.definition.tags + +import com.vitorpamplona.quartz.nip01Core.core.Tag +import com.vitorpamplona.quartz.nip89AppHandlers.PlatformType +import com.vitorpamplona.quartz.utils.arrayOfNotNull + +class PlatformLinkTag( + val platform: String, + val uri: String, + val entityType: String?, +) { + fun toTagArray() = assemble(platform, uri, entityType) + + companion object { + const val TAG_SIZE = 3 + + @JvmStatic + fun match(tag: Tag): Boolean = + if (tag.size >= TAG_SIZE) { + tag[0] == PlatformType.IOS.code || + tag[0] == PlatformType.WEB.code || + tag[0] == PlatformType.ANDROID.code + } else { + false + } + + @JvmStatic + fun parse(tag: Tag): PlatformLinkTag? { + if (match(tag)) return PlatformLinkTag(tag[1], tag[2], tag.getOrNull(3)) + return null + } + + @JvmStatic + fun assemble( + platform: String, + uri: String, + entityType: String?, + ) = arrayOfNotNull(platform, uri, entityType) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/CommunityDefinitionEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/recommendation/AppRecommendationEvent.kt similarity index 52% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/CommunityDefinitionEvent.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/recommendation/AppRecommendationEvent.kt index a208d89c5..70737bb42 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/CommunityDefinitionEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/recommendation/AppRecommendationEvent.kt @@ -18,18 +18,22 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip72ModCommunities +package com.vitorpamplona.quartz.nip89AppHandlers.recommendation import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.experimental.audio.Participant import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag +import com.vitorpamplona.quartz.nip31Alts.alt +import com.vitorpamplona.quartz.nip89AppHandlers.PlatformType +import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent +import com.vitorpamplona.quartz.nip89AppHandlers.recommendation.tags.RecommendationTag import com.vitorpamplona.quartz.utils.TimeUtils @Immutable -class CommunityDefinitionEvent( +class AppRecommendationEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -37,26 +41,32 @@ class CommunityDefinitionEvent( content: String, sig: HexKey, ) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) { - fun description() = tags.firstOrNull { it.size > 1 && it[0] == "description" }?.get(1) + fun recommendations() = tags.mapNotNull(RecommendationTag::parse) - fun image() = tags.firstOrNull { it.size > 1 && it[0] == "image" }?.get(1) - - fun rules() = tags.firstOrNull { it.size > 1 && it[0] == "rules" }?.get(1) - - fun moderators() = tags.filter { it.size > 1 && it[0] == "p" }.map { Participant(it[1], it.getOrNull(3)) } + fun recommendationAddresses() = tags.mapNotNull(RecommendationTag::parseAddress) companion object { - const val KIND = 34550 - const val ALT = "Community definition" + const val KIND = 31989 + const val ALT_DESCRIPTION = "App recommendations by the author" - fun create( - signer: NostrSigner, + class AppRecommendationItem( + val appDefinitionEvent: AppDefinitionEvent, + val relayHint: String?, + val platform: PlatformType, + ) + + fun build( + supportedKind: String, + appReferences: List, createdAt: Long = TimeUtils.now(), - onReady: (CommunityDefinitionEvent) -> Unit, - ) { - val tags = mutableListOf>() - tags.add(AltTagSerializer.toTagArray(ALT)) - signer.sign(createdAt, KIND, tags.toTypedArray(), "", onReady) + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, "", createdAt) { + dTag(supportedKind) + appReferences.forEach { + recommend(it.appDefinitionEvent.addressTag(), it.relayHint, it.platform.code) + } + alt(ALT_DESCRIPTION) + initializer() } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/recommendation/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/recommendation/TagArrayBuilderExt.kt new file mode 100644 index 000000000..7df2ad8b0 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/recommendation/TagArrayBuilderExt.kt @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip89AppHandlers.recommendation + +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address +import com.vitorpamplona.quartz.nip89AppHandlers.recommendation.tags.RecommendationTag + +fun TagArrayBuilder.recommend( + addressId: String, + relay: String?, + platform: String?, +) = add(RecommendationTag.assemble(addressId, relay, platform)) + +fun TagArrayBuilder.recommend( + address: Address, + relay: String?, + platform: String?, +) = add(RecommendationTag.assemble(address, relay, platform)) + +fun TagArrayBuilder.recommend(tag: RecommendationTag) = add(tag.toTagArray()) + +fun TagArrayBuilder.recommend(tags: List) = addAll(tags.map { it.toTagArray() }) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/recommendation/tags/RecommendationTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/recommendation/tags/RecommendationTag.kt new file mode 100644 index 000000000..0b094b2b4 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/recommendation/tags/RecommendationTag.kt @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip89AppHandlers.recommendation.tags + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.core.Tag +import com.vitorpamplona.quartz.nip01Core.core.match +import com.vitorpamplona.quartz.nip01Core.core.valueIfMatches +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address +import com.vitorpamplona.quartz.utils.arrayOfNotNull + +@Immutable +class RecommendationTag( + val address: Address, + val relay: String? = null, + val platform: String? = null, +) { + fun toTagArray() = assemble(address, relay, platform) + + companion object { + const val TAG_NAME = "a" + const val TAG_SIZE = 2 + + @JvmStatic + fun match(tag: Tag) = tag.match(TAG_NAME, TAG_SIZE) + + @JvmStatic + fun parse(tag: Array): RecommendationTag? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + val address = Address.parse(tag[1]) ?: return null + return RecommendationTag(address, tag.getOrNull(2), tag.getOrNull(3)) + } + + @JvmStatic + fun parseAddress(tag: Array) = tag.valueIfMatches(TAG_NAME, TAG_SIZE) + + @JvmStatic + fun assemble( + addressId: String, + relay: String?, + platform: String?, + ) = arrayOfNotNull(TAG_NAME, addressId, relay, platform) + + @JvmStatic + fun assemble( + address: Address, + relay: String?, + platform: String?, + ) = arrayOfNotNull(TAG_NAME, address.toValue(), relay, platform) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip90Dvms/NIP90ContentDiscoveryRequestEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip90Dvms/NIP90ContentDiscoveryRequestEvent.kt index da39dc3a2..6335f4c70 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip90Dvms/NIP90ContentDiscoveryRequestEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip90Dvms/NIP90ContentDiscoveryRequestEvent.kt @@ -25,7 +25,7 @@ import androidx.compose.runtime.Stable import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer +import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.utils.TimeUtils @Stable @@ -53,7 +53,7 @@ class NIP90ContentDiscoveryRequestEvent( val content = "" val tags = mutableListOf>() tags.add(arrayOf("p", dvmPublicKey)) - tags.add(AltTagSerializer.toTagArray(ALT)) + tags.add(AltTag.assemble(ALT)) tags.add(arrayOf("relays") + relays.toTypedArray()) tags.add(arrayOf("param", "max_results", "200")) tags.add(arrayOf("param", "user", forUser)) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip90Dvms/NIP90ContentDiscoveryResponseEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip90Dvms/NIP90ContentDiscoveryResponseEvent.kt index 790de2176..fe1e93ac2 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip90Dvms/NIP90ContentDiscoveryResponseEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip90Dvms/NIP90ContentDiscoveryResponseEvent.kt @@ -27,8 +27,8 @@ import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer -import com.vitorpamplona.quartz.nip89AppHandlers.AppRecommendationEvent +import com.vitorpamplona.quartz.nip31Alts.AltTag +import com.vitorpamplona.quartz.nip89AppHandlers.recommendation.AppRecommendationEvent import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.pointerSizeInBytes @@ -84,7 +84,7 @@ class NIP90ContentDiscoveryResponseEvent( ) { val tags = arrayOf( - AltTagSerializer.toTagArray(ALT), + AltTag.assemble(ALT), ) signer.sign(createdAt, KIND, tags, "", onReady) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip90Dvms/NIP90StatusEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip90Dvms/NIP90StatusEvent.kt index a01893037..97784762e 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip90Dvms/NIP90StatusEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip90Dvms/NIP90StatusEvent.kt @@ -24,8 +24,8 @@ import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer -import com.vitorpamplona.quartz.nip89AppHandlers.AppRecommendationEvent +import com.vitorpamplona.quartz.nip31Alts.AltTag +import com.vitorpamplona.quartz.nip89AppHandlers.recommendation.AppRecommendationEvent import com.vitorpamplona.quartz.utils.TimeUtils @Immutable @@ -85,7 +85,7 @@ class NIP90StatusEvent( ) { val tags = arrayOf( - AltTagSerializer.toTagArray(ALT), + AltTag.assemble(ALT), ) signer.sign(createdAt, KIND, tags, "", onReady) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip90Dvms/NIP90UserDiscoveryRequestEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip90Dvms/NIP90UserDiscoveryRequestEvent.kt index 7a16cf522..9b12ccbd0 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip90Dvms/NIP90UserDiscoveryRequestEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip90Dvms/NIP90UserDiscoveryRequestEvent.kt @@ -24,8 +24,8 @@ import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer -import com.vitorpamplona.quartz.nip89AppHandlers.AppRecommendationEvent +import com.vitorpamplona.quartz.nip31Alts.AltTag +import com.vitorpamplona.quartz.nip89AppHandlers.recommendation.AppRecommendationEvent import com.vitorpamplona.quartz.utils.TimeUtils @Immutable @@ -48,7 +48,7 @@ class NIP90UserDiscoveryRequestEvent( ) { val tags = arrayOf( - AltTagSerializer.toTagArray(ALT), + AltTag.assemble(ALT), ) signer.sign(createdAt, KIND, tags, "", onReady) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip90Dvms/NIP90UserDiscoveryResponseEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip90Dvms/NIP90UserDiscoveryResponseEvent.kt index 1239dd41d..6528f0ac3 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip90Dvms/NIP90UserDiscoveryResponseEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip90Dvms/NIP90UserDiscoveryResponseEvent.kt @@ -24,8 +24,8 @@ import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer -import com.vitorpamplona.quartz.nip89AppHandlers.AppRecommendationEvent +import com.vitorpamplona.quartz.nip31Alts.AltTag +import com.vitorpamplona.quartz.nip89AppHandlers.recommendation.AppRecommendationEvent import com.vitorpamplona.quartz.utils.TimeUtils @Immutable @@ -48,7 +48,7 @@ class NIP90UserDiscoveryResponseEvent( ) { val tags = arrayOf( - AltTagSerializer.toTagArray(ALT), + AltTag.assemble(ALT), ) signer.sign(createdAt, KIND, tags, "", onReady) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip35Torrents/TorrentFile.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip92IMeta/EventExt.kt similarity index 86% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip35Torrents/TorrentFile.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip92IMeta/EventExt.kt index 7b346585e..7e706cd3f 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip35Torrents/TorrentFile.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip92IMeta/EventExt.kt @@ -18,12 +18,8 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip35Torrents +package com.vitorpamplona.quartz.nip92IMeta -import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.core.Event -@Immutable -class TorrentFile( - val fileName: String, - val bytes: Long?, -) +fun Event.imetas() = tags.imetas() diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip92IMeta/IMetaTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip92IMeta/IMetaTag.kt index 1c4ae17fc..7f8c1c8d1 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip92IMeta/IMetaTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip92IMeta/IMetaTag.kt @@ -22,5 +22,51 @@ package com.vitorpamplona.quartz.nip92IMeta class IMetaTag( val url: String, - val properties: Map, -) + val properties: Map>, +) { + fun toTagArray() = + arrayOf(TAG_NAME, "$ANCHOR_PROPERTY $url") + + properties + .mapNotNull { (key, value) -> + if (key != ANCHOR_PROPERTY) { + value.map { "$key $it" } + } else { + null + } + }.flatten() + + companion object { + const val TAG_NAME = "imeta" + const val ANCHOR_PROPERTY = "url" + const val TAG_SIZE = 2 + + fun parse(tag: Array): IMetaTag? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + + val allTags = parseIMeta(tag) + val url = allTags.get(ANCHOR_PROPERTY)?.firstOrNull() + + return if (url != null) { + IMetaTag(url, allTags.minus(ANCHOR_PROPERTY)) + } else { + null + } + } + + private fun parseIMeta(tags: Array): Map> { + val propertiesByKey = mutableMapOf>() + + tags.forEach { tag -> + val parts = tag.split(" ", limit = 2) + when (parts.size) { + 2 -> propertiesByKey.getOrPut(parts[0], { mutableListOf() }).add(parts[1]) + 1 -> propertiesByKey.getOrPut(parts[0], { mutableListOf() }).add("") + } + } + + return propertiesByKey + } + + fun assemble(iMetaTag: IMetaTag) = iMetaTag.toTagArray() + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip92IMeta/IMetaTagBuilder.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip92IMeta/IMetaTagBuilder.kt index ac81bfeef..1ee58482b 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip92IMeta/IMetaTagBuilder.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip92IMeta/IMetaTagBuilder.kt @@ -20,51 +20,18 @@ */ package com.vitorpamplona.quartz.nip92IMeta -import com.vitorpamplona.quartz.nip01Core.HexKey -import com.vitorpamplona.quartz.nip36SensitiveContent.CONTENT_WARNING -import com.vitorpamplona.quartz.nip94FileMetadata.Dimension -import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent.Companion.ALT -import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent.Companion.BLUR_HASH -import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent.Companion.DIMENSION -import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent.Companion.FILE_SIZE -import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent.Companion.HASH -import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent.Companion.MAGNET_URI -import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent.Companion.MIME_TYPE -import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent.Companion.ORIGINAL_HASH -import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent.Companion.TORRENT_INFOHASH - class IMetaTagBuilder( val url: String, ) { - val properties = mutableMapOf() + val properties = mutableMapOf>() fun add( key: String, value: String, ): IMetaTagBuilder { - properties.set(key, value) + properties.getOrPut(key) { mutableListOf() }.add(value) return this } - fun magnet(uri: String) = add(MAGNET_URI, uri) - - fun mimeType(mime: String) = add(MIME_TYPE, mime) - - fun alt(alt: String) = add(ALT, alt) - - fun hash(hash: HexKey) = add(HASH, hash) - - fun size(size: Int) = add(FILE_SIZE, size.toString()) - - fun dims(dims: Dimension) = add(DIMENSION, dims.toString()) - - fun blurhash(blurhash: String) = add(BLUR_HASH, blurhash) - - fun originalHash(originalHash: String) = add(ORIGINAL_HASH, originalHash) - - fun torrent(uri: String) = add(TORRENT_INFOHASH, uri) - - fun sensitiveContent(reason: String) = add(CONTENT_WARNING, reason) - fun build() = IMetaTag(url, properties) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip92IMeta/Nip92MediaAttachments.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip92IMeta/Nip92MediaAttachments.kt deleted file mode 100644 index 821091951..000000000 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip92IMeta/Nip92MediaAttachments.kt +++ /dev/null @@ -1,76 +0,0 @@ -/** - * Copyright (c) 2024 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.quartz.nip92IMeta - -class Nip92MediaAttachments { - companion object { - const val IMETA = "imeta" - - fun createTag(header: IMetaTag): Array = - createTag( - header.url, - header.properties, - ) - - fun createTag( - url: String, - tags: Map, - ): Array = - arrayOf( - IMETA, - "url $url", - ) + - tags.mapNotNull { - if (it.key != "url") { - "${it.key} ${it.value}" - } else { - null - } - } - - fun parse( - url: String, - tags: Array>, - ): Map = - tags - .firstOrNull { - it.size > 1 && it[0] == IMETA && it[1] == "url $url" - }?.let { tagList -> - parseIMeta(tagList) - } ?: emptyMap() - - fun parse(tags: Array>): Map> = - tags.filter { it.size > 1 && it[0] == IMETA }.associate { - val allTags = parseIMeta(it) - (allTags.get("url") ?: "") to allTags - } - - private fun parseIMeta(tags: Array): Map = - tags.associate { tag -> - val parts = tag.split(" ", limit = 2) - when (parts.size) { - 2 -> parts[0] to parts[1] - 1 -> parts[0] to "" - else -> "" to "" - } - } - } -} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip92IMeta/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip92IMeta/TagArrayBuilderExt.kt new file mode 100644 index 000000000..d7201ae7f --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip92IMeta/TagArrayBuilderExt.kt @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip92IMeta + +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder + +fun TagArrayBuilder.imeta(tag: IMetaTag) = add(tag.toTagArray()) + +fun TagArrayBuilder.imetas(tags: List) = addAll(tags.map { it.toTagArray() }) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip92IMeta/TagArrayExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip92IMeta/TagArrayExt.kt new file mode 100644 index 000000000..27bfb5643 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip92IMeta/TagArrayExt.kt @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip92IMeta + +import com.vitorpamplona.quartz.nip01Core.core.TagArray + +fun TagArray.imetas() = this.mapNotNull(IMetaTag::parse) + +fun TagArray.imetasByUrl() = this.imetas().associateBy { it.url } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/FileHeaderEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/FileHeaderEvent.kt index 7c35038ef..d61a777ab 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/FileHeaderEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/FileHeaderEvent.kt @@ -23,9 +23,23 @@ package com.vitorpamplona.quartz.nip94FileMetadata import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer -import com.vitorpamplona.quartz.nip36SensitiveContent.ContentWarningSerializer +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.core.any +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip31Alts.alt +import com.vitorpamplona.quartz.nip94FileMetadata.tags.BlurhashTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.FallbackTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.HashTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.ImageTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.MagnetTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.MimeTypeTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.ServiceTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.SizeTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.SummaryTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.ThumbTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.TorrentInfoHash +import com.vitorpamplona.quartz.nip94FileMetadata.tags.UrlTag import com.vitorpamplona.quartz.utils.TimeUtils @Immutable @@ -37,102 +51,80 @@ class FileHeaderEvent( content: String, sig: HexKey, ) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { - fun url() = tags.firstOrNull { it.size > 1 && it[0] == URL }?.get(1) + fun url() = tags.firstNotNullOfOrNull(UrlTag::parse) - fun urls() = tags.filter { it.size > 1 && it[0] == URL }.map { it[1] } + fun urls() = tags.mapNotNull(UrlTag::parse) - fun mimeType() = tags.firstOrNull { it.size > 1 && it[0] == MIME_TYPE }?.get(1) + fun mimeType() = tags.firstNotNullOfOrNull(MimeTypeTag::parse) - fun hash() = tags.firstOrNull { it.size > 1 && it[0] == HASH }?.get(1) + fun hash() = tags.firstNotNullOfOrNull(HashTag::parse) - fun size() = tags.firstOrNull { it.size > 1 && it[0] == FILE_SIZE }?.get(1) + fun size() = tags.firstNotNullOfOrNull(SizeTag::parse) - fun alt() = tags.firstOrNull { it.size > 1 && it[0] == ALT }?.get(1) + fun dimensions() = tags.firstNotNullOfOrNull(DimensionTag::parse) - fun dimensions() = tags.firstOrNull { it.size > 1 && it[0] == DIMENSION }?.get(1)?.let { Dimension.parse(it) } + fun magnetURI() = tags.firstNotNullOfOrNull(MagnetTag::parse) - fun magnetURI() = tags.firstOrNull { it.size > 1 && it[0] == MAGNET_URI }?.get(1) + fun torrentInfoHash() = tags.firstNotNullOfOrNull(TorrentInfoHash::parse) - fun torrentInfoHash() = tags.firstOrNull { it.size > 1 && it[0] == TORRENT_INFOHASH }?.get(1) + fun blurhash() = tags.firstNotNullOfOrNull(BlurhashTag::parse) - fun blurhash() = tags.firstOrNull { it.size > 1 && it[0] == BLUR_HASH }?.get(1) + fun image() = tags.firstNotNullOfOrNull(ImageTag::parse) - fun hasUrl() = tags.any { it.size > 1 && it[0] == URL } + fun thumb() = tags.firstNotNullOfOrNull(ThumbTag::parse) - fun isOneOf(mimeTypes: Set) = tags.any { it.size > 1 && it[0] == MIME_TYPE && mimeTypes.contains(it[1]) } + fun service() = tags.firstNotNullOfOrNull(ServiceTag::parse) + + fun summary() = tags.firstNotNullOfOrNull(SummaryTag::parse) + + fun fallback() = tags.firstNotNullOfOrNull(FallbackTag::parse) + + fun hasUrl() = tags.any(UrlTag::isTag) + + fun isOneOf(mimeTypes: Set) = tags.any(MimeTypeTag::isIn, mimeTypes) companion object { const val KIND = 1063 const val ALT_DESCRIPTION = "Verifiable file url" - const val URL = "url" - const val ENCRYPTION_KEY = "aes-256-gcm" - const val MIME_TYPE = "m" - const val FILE_SIZE = "size" - const val DIMENSION = "dim" - const val HASH = "x" - const val MAGNET_URI = "magnet" - const val TORRENT_INFOHASH = "i" - const val BLUR_HASH = "blurhash" - const val ORIGINAL_HASH = "ox" - const val ALT = "alt" - - fun buildTags( + fun build( url: String, - magnetUri: String? = null, - mimeType: String? = null, - alt: String? = null, - hash: String? = null, - size: String? = null, - dimensions: Dimension? = null, - blurhash: String? = null, - originalHash: String? = null, - magnetURI: String? = null, - torrentInfoHash: String? = null, - sensitiveContent: Boolean? = null, - ): Array> = - listOfNotNull( - arrayOf(URL, url), - magnetUri?.let { arrayOf(MAGNET_URI, it) }, - mimeType?.let { arrayOf(MIME_TYPE, it) }, - alt?.ifBlank { null }?.let { arrayOf(ALT, it) } ?: AltTagSerializer.toTagArray(ALT_DESCRIPTION), - hash?.let { arrayOf(HASH, it) }, - size?.let { arrayOf(FILE_SIZE, it) }, - dimensions?.let { arrayOf(DIMENSION, it.toString()) }, - blurhash?.let { arrayOf(BLUR_HASH, it) }, - originalHash?.let { arrayOf(ORIGINAL_HASH, it) }, - magnetURI?.let { arrayOf(MAGNET_URI, it) }, - torrentInfoHash?.let { arrayOf(TORRENT_INFOHASH, it) }, - sensitiveContent?.let { - if (it) { - ContentWarningSerializer.toTagArray() - } else { - null - } - }, - ).toTypedArray() - - fun create( - url: String, - magnetUri: String? = null, - mimeType: String? = null, - alt: String? = null, - hash: String? = null, - size: String? = null, - dimensions: Dimension? = null, - blurhash: String? = null, - originalHash: String? = null, - magnetURI: String? = null, - torrentInfoHash: String? = null, - sensitiveContent: Boolean? = null, - signer: NostrSigner, + caption: String?, createdAt: Long = TimeUtils.now(), - onReady: (FileHeaderEvent) -> Unit, - ) { - val tags = buildTags(url, magnetUri, mimeType, alt, hash, size, dimensions, blurhash, originalHash, magnetURI, torrentInfoHash, sensitiveContent) + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, caption ?: "", createdAt) { + url(url) + caption?.ifBlank { null }?.let { alt(caption) } ?: alt(ALT_DESCRIPTION) + initializer() + } - val content = alt ?: "" - signer.sign(createdAt, KIND, tags, content, onReady) + fun build( + url: String, + caption: String?, + mimeType: String? = null, + hash: String? = null, + size: Int? = null, + dimension: DimensionTag? = null, + blurhash: String? = null, + originalHash: String? = null, + magnetUri: String? = null, + torrentInfoHash: String? = null, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, caption ?: "", createdAt) { + url(url) + caption?.ifBlank { null }?.let { alt(caption) } ?: alt(ALT_DESCRIPTION) + + hash?.let { hash(it) } + size?.let { fileSize(it) } + mimeType?.let { mimeType(it) } + dimension?.let { dimension(it) } + blurhash?.let { blurhash(it) } + originalHash?.let { originalHash(it) } + magnetUri?.let { magnet(it) } + torrentInfoHash?.let { torrentInfohash(it) } + + initializer() } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/IMetaTagBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/IMetaTagBuilderExt.kt new file mode 100644 index 000000000..ff8615119 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/IMetaTagBuilderExt.kt @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip94FileMetadata + +import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip31Alts.AltTag +import com.vitorpamplona.quartz.nip36SensitiveContent.ContentWarningTag +import com.vitorpamplona.quartz.nip92IMeta.IMetaTagBuilder +import com.vitorpamplona.quartz.nip94FileMetadata.tags.BlurhashTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.FallbackTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.HashTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.ImageTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.MagnetTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.MimeTypeTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.OriginalHashTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.ServiceTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.SizeTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.SummaryTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.ThumbTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.TorrentInfoHash + +/** + * Contains the IMeta tags that are used by Picture events. + */ +fun IMetaTagBuilder.magnet(uri: String) = add(MagnetTag.TAG_NAME, uri) + +fun IMetaTagBuilder.mimeType(mime: String) = add(MimeTypeTag.TAG_NAME, mime) + +fun IMetaTagBuilder.alt(alt: String) = add(AltTag.TAG_NAME, alt) + +fun IMetaTagBuilder.hash(hash: HexKey) = add(HashTag.TAG_NAME, hash) + +fun IMetaTagBuilder.size(size: Int) = add(SizeTag.TAG_NAME, size.toString()) + +fun IMetaTagBuilder.dims(dims: DimensionTag) = add(DimensionTag.TAG_NAME, dims.toString()) + +fun IMetaTagBuilder.blurhash(blurhash: String) = add(BlurhashTag.TAG_NAME, blurhash) + +fun IMetaTagBuilder.originalHash(originalHash: String) = add(OriginalHashTag.TAG_NAME, originalHash) + +fun IMetaTagBuilder.torrent(uri: String) = add(TorrentInfoHash.TAG_NAME, uri) + +fun IMetaTagBuilder.sensitiveContent(reason: String) = add(ContentWarningTag.TAG_NAME, reason) + +fun IMetaTagBuilder.image(imageUrl: HexKey) = add(ImageTag.TAG_NAME, imageUrl) + +fun IMetaTagBuilder.thumb(thumbUrl: HexKey) = add(ThumbTag.TAG_NAME, thumbUrl) + +fun IMetaTagBuilder.summary(summary: HexKey) = add(SummaryTag.TAG_NAME, summary) + +fun IMetaTagBuilder.fallback(fallback: HexKey) = add(FallbackTag.TAG_NAME, fallback) + +fun IMetaTagBuilder.service(service: HexKey) = add(ServiceTag.TAG_NAME, service) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/TagArrayBuilderExt.kt new file mode 100644 index 000000000..cd4dfb489 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/TagArrayBuilderExt.kt @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip94FileMetadata + +import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip94FileMetadata.tags.BlurhashTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.FallbackTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.HashTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.ImageTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.MagnetTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.MimeTypeTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.OriginalHashTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.ServiceTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.SizeTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.SummaryTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.ThumbTag +import com.vitorpamplona.quartz.nip94FileMetadata.tags.TorrentInfoHash +import com.vitorpamplona.quartz.nip94FileMetadata.tags.UrlTag + +fun TagArrayBuilder.url(url: String) = add(UrlTag.assemble(url)) + +fun TagArrayBuilder.mimeType(mimeType: String) = add(MimeTypeTag.assemble(mimeType)) + +fun TagArrayBuilder.hash(hash: HexKey) = add(HashTag.assemble(hash)) + +fun TagArrayBuilder.fileSize(size: Int) = add(SizeTag.assemble(size)) + +fun TagArrayBuilder.dimension(dim: DimensionTag) = add(DimensionTag.assemble(dim)) + +fun TagArrayBuilder.blurhash(blurhash: String) = add(BlurhashTag.assemble(blurhash)) + +fun TagArrayBuilder.originalHash(hash: HexKey) = add(OriginalHashTag.assemble(hash)) + +fun TagArrayBuilder.torrentInfohash(hash: String) = add(TorrentInfoHash.assemble(hash)) + +fun TagArrayBuilder.magnet(magnetUri: String) = add(MagnetTag.assemble(magnetUri)) + +fun TagArrayBuilder.image(imageUrl: HexKey) = add(ImageTag.assemble(imageUrl)) + +fun TagArrayBuilder.thumb(trumbUrl: HexKey) = add(ThumbTag.assemble(trumbUrl)) + +fun TagArrayBuilder.summary(summary: HexKey) = add(SummaryTag.assemble(summary)) + +fun TagArrayBuilder.fallback(fallbackUrl: HexKey) = add(FallbackTag.assemble(fallbackUrl)) + +fun TagArrayBuilder.service(service: HexKey) = add(ServiceTag.assemble(service)) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/BlurhashTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/BlurhashTag.kt new file mode 100644 index 000000000..329314d84 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/BlurhashTag.kt @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip94FileMetadata.tags + +class BlurhashTag { + companion object { + const val TAG_NAME = "blurhash" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): String? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1] + } + + @JvmStatic + fun assemble(hash: String) = arrayOf(TAG_NAME, hash) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/Dimension.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/DimensionTag.kt similarity index 74% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/Dimension.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/DimensionTag.kt index 3936ba330..dcfd011ca 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/Dimension.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/DimensionTag.kt @@ -18,9 +18,9 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip94FileMetadata +package com.vitorpamplona.quartz.nip94FileMetadata.tags -class Dimension( +class DimensionTag( val width: Int, val height: Int, ) { @@ -30,8 +30,20 @@ class Dimension( override fun toString() = "${width}x$height" + fun toTagArray() = assemble(this) + companion object { - fun parse(dim: String): Dimension? { + const val TAG_NAME = "dim" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): DimensionTag? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return parse(tag[1]) + } + + @JvmStatic + fun parse(dim: String): DimensionTag? { if (dim == "0x0") return null val parts = dim.split("x") @@ -41,10 +53,13 @@ class Dimension( val width = parts[0].toInt() val height = parts[1].toInt() - Dimension(width, height) + DimensionTag(width, height) } catch (e: Exception) { null } } + + @JvmStatic + fun assemble(dim: DimensionTag) = arrayOf(TAG_NAME, dim.toString()) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/FallbackTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/FallbackTag.kt new file mode 100644 index 000000000..4eca6fd06 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/FallbackTag.kt @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip94FileMetadata.tags + +class FallbackTag { + companion object { + const val TAG_NAME = "fallback" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): String? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1] + } + + @JvmStatic + fun assemble(imageUrl: String) = arrayOf(TAG_NAME, imageUrl) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/HashTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/HashTag.kt new file mode 100644 index 000000000..04f7d4a9a --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/HashTag.kt @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip94FileMetadata.tags + +class HashTag { + companion object { + const val TAG_NAME = "x" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): String? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1] + } + + @JvmStatic + fun assemble(hash: String) = arrayOf(TAG_NAME, hash) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/ImageTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/ImageTag.kt new file mode 100644 index 000000000..38571c230 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/ImageTag.kt @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip94FileMetadata.tags + +import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.utils.arrayOfNotNull + +class ImageTag( + val imageUrl: String, + val hash: HexKey?, +) { + companion object { + const val TAG_NAME = "image" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): ImageTag? { + if (tag.size < TAG_SIZE || tag[0] != ThumbTag.TAG_NAME) return null + return ImageTag(tag[1], tag.getOrNull(2)) + } + + @JvmStatic + fun assemble( + imageUrl: String, + hash: String? = null, + ) = arrayOfNotNull(TAG_NAME, imageUrl, hash) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/MagnetTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/MagnetTag.kt new file mode 100644 index 000000000..9bd916cd7 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/MagnetTag.kt @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip94FileMetadata.tags + +class MagnetTag { + companion object { + const val TAG_NAME = "magnet" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): String? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1] + } + + @JvmStatic + fun assemble(url: String) = arrayOf(TAG_NAME, url) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/MimeTypeTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/MimeTypeTag.kt new file mode 100644 index 000000000..416b3f0a1 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/MimeTypeTag.kt @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip94FileMetadata.tags + +class MimeTypeTag { + companion object { + const val TAG_NAME = "m" + const val TAG_SIZE = 2 + + fun isIn( + tag: Array, + mimeTypes: Set, + ) = tag.size >= TAG_SIZE && tag[0] == TAG_NAME && tag[1] in mimeTypes + + @JvmStatic + fun parse(tag: Array): String? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1] + } + + @JvmStatic + fun assemble(mimeType: String) = arrayOf(TAG_NAME, mimeType) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/OriginalHashTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/OriginalHashTag.kt new file mode 100644 index 000000000..6cc31f954 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/OriginalHashTag.kt @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip94FileMetadata.tags + +class OriginalHashTag { + companion object { + const val TAG_NAME = "ox" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): String? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1] + } + + @JvmStatic + fun assemble(hash: String) = arrayOf(TAG_NAME, hash) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/ServiceTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/ServiceTag.kt new file mode 100644 index 000000000..ca4703c05 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/ServiceTag.kt @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip94FileMetadata.tags + +class ServiceTag { + companion object { + const val TAG_NAME = "service" + const val TAG_SIZE = 2 + + @JvmStatic + fun isTag(tag: Array) = tag.size >= TAG_SIZE && tag[0] == TAG_NAME + + @JvmStatic + fun parse(tag: Array): String? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1] + } + + @JvmStatic + fun assemble(serviceType: String) = arrayOf(TAG_NAME, serviceType) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/SizeTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/SizeTag.kt new file mode 100644 index 000000000..4548ea5f5 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/SizeTag.kt @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip94FileMetadata.tags + +class SizeTag { + companion object { + const val TAG_NAME = "size" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): Int? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1].toIntOrNull() + } + + @JvmStatic + fun assemble(sizeInBytes: Int) = arrayOf(TAG_NAME, sizeInBytes.toString()) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/SummaryTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/SummaryTag.kt new file mode 100644 index 000000000..b854ab7a6 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/SummaryTag.kt @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip94FileMetadata.tags + +class SummaryTag { + companion object { + const val TAG_NAME = "summary" + const val TAG_SIZE = 2 + + @JvmStatic + fun isTag(tag: Array) = tag.size >= TAG_SIZE && tag[0] == TAG_NAME + + @JvmStatic + fun parse(tag: Array): String? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1] + } + + @JvmStatic + fun assemble(excerpt: String) = arrayOf(TAG_NAME, excerpt) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/ThumbTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/ThumbTag.kt new file mode 100644 index 000000000..cd4d20cd0 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/ThumbTag.kt @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip94FileMetadata.tags + +import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.utils.arrayOfNotNull + +class ThumbTag( + val imageUrl: String, + val hash: HexKey?, +) { + companion object { + const val TAG_NAME = "thumb" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): ThumbTag? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return ThumbTag(tag[1], tag.getOrNull(2)) + } + + @JvmStatic + fun assemble( + imageUrl: String, + hash: String? = null, + ) = arrayOfNotNull(TAG_NAME, imageUrl, hash) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/TorrentInfoHash.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/TorrentInfoHash.kt new file mode 100644 index 000000000..93b87b03e --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/TorrentInfoHash.kt @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip94FileMetadata.tags + +class TorrentInfoHash { + companion object { + const val TAG_NAME = "i" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): String? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1] + } + + @JvmStatic + fun assemble(url: String) = arrayOf(TAG_NAME, url) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/UrlTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/UrlTag.kt new file mode 100644 index 000000000..4be128be8 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/UrlTag.kt @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip94FileMetadata.tags + +class UrlTag { + companion object { + const val TAG_NAME = "url" + const val TAG_SIZE = 2 + + @JvmStatic + fun isTag(tag: Array) = tag.size >= TAG_SIZE && tag[0] == TAG_NAME + + @JvmStatic + fun parse(tag: Array): String? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1] + } + + @JvmStatic + fun assemble(url: String) = arrayOf(TAG_NAME, url) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip96FileStorage/ResultParser.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip96FileStorage/actions/DeleteResult.kt similarity index 70% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip96FileStorage/ResultParser.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip96FileStorage/actions/DeleteResult.kt index f456788b0..db1101e2b 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip96FileStorage/ResultParser.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip96FileStorage/actions/DeleteResult.kt @@ -18,19 +18,19 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip96FileStorage +package com.vitorpamplona.quartz.nip96FileStorage.actions import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -class ResultParser { - fun parseDeleteResults(body: String): DeleteResult { - val mapper = jacksonObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - return mapper.readValue(body, DeleteResult::class.java) - } - - fun parseResults(body: String): Nip96Result { - val mapper = jacksonObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - return mapper.readValue(body, Nip96Result::class.java) +data class DeleteResult( + val status: String?, + val message: String?, +) { + companion object { + fun parse(body: String): DeleteResult { + val mapper = jacksonObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + return mapper.readValue(body, DeleteResult::class.java) + } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip96FileStorage/Nip96Result.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip96FileStorage/actions/UploadResult.kt similarity index 75% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip96FileStorage/Nip96Result.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip96FileStorage/actions/UploadResult.kt index 6bdba9dd1..8d53e3c14 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip96FileStorage/Nip96Result.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip96FileStorage/actions/UploadResult.kt @@ -18,11 +18,13 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip96FileStorage +package com.vitorpamplona.quartz.nip96FileStorage.actions import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -data class Nip96Result( +data class UploadResult( val status: String? = null, val message: String? = null, @JsonProperty("processing_url") @@ -30,14 +32,16 @@ data class Nip96Result( val percentage: Int? = null, @JsonProperty("nip94_event") val nip94Event: PartialEvent? = null, -) +) { + companion object { + fun parse(body: String): UploadResult { + val mapper = jacksonObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + return mapper.readValue(body, UploadResult::class.java) + } + } +} class PartialEvent( val tags: Array>? = null, val content: String? = null, ) - -data class DeleteResult( - val status: String?, - val message: String?, -) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip96FileStorage/FileServersEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip96FileStorage/config/FileServersEvent.kt similarity index 57% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip96FileStorage/FileServersEvent.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip96FileStorage/config/FileServersEvent.kt index 242a7f1e6..072418170 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip96FileStorage/FileServersEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip96FileStorage/config/FileServersEvent.kt @@ -18,14 +18,16 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip96FileStorage +package com.vitorpamplona.quartz.nip96FileStorage.config import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.BaseReplaceableEvent -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer +import com.vitorpamplona.quartz.nip31Alts.alt +import com.vitorpamplona.quartz.nip96FileStorage.config.tags.ServerTag import com.vitorpamplona.quartz.utils.TimeUtils @Immutable @@ -37,65 +39,41 @@ class FileServersEvent( content: String, sig: HexKey, ) : BaseReplaceableEvent(id, pubKey, createdAt, KIND, tags, content, sig) { - fun servers(): List = - tags.mapNotNull { - if (it.size > 1 && it[0] == "server") { - it[1] - } else { - null - } - } + fun servers(): List = tags.mapNotNull(ServerTag::parse) companion object { const val KIND = 10096 - const val ALT = "File servers used by the author" + const val ALT_DESCRIPTOR = "File servers used by the author" fun createAddressATag(pubKey: HexKey): ATag = ATag(KIND, pubKey, FIXED_D_TAG, null) fun createAddressTag(pubKey: HexKey): String = ATag.assembleATagId(KIND, pubKey, FIXED_D_TAG) - fun createTagArray(servers: List): Array> = - servers - .map { - arrayOf("server", it) - }.plusElement(AltTagSerializer.toTagArray(ALT)) - .toTypedArray() - - fun updateRelayList( + fun replaceServers( earlierVersion: FileServersEvent, relays: List, - signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (FileServersEvent) -> Unit, - ) { - val tags = - earlierVersion.tags - .filter { it[0] != "server" } - .plus( - relays.map { - arrayOf("server", it) - }, - ).toTypedArray() - - signer.sign(createdAt, KIND, tags, earlierVersion.content, onReady) + ) = eventTemplate(KIND, earlierVersion.content, createdAt) { + remove(ServerTag.TAG_NAME) + servers(relays) } - fun createFromScratch( - relays: List, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (FileServersEvent) -> Unit, - ) { - create(relays, signer, createdAt, onReady) - } - - fun create( + fun build( servers: List, - signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (FileServersEvent) -> Unit, - ) { - signer.sign(createdAt, KIND, createTagArray(servers), "", onReady) + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, "", createdAt) { + alt(ALT_DESCRIPTOR) + servers(servers) + initializer() + } + + fun build( + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, "", createdAt) { + alt(ALT_DESCRIPTOR) + initializer() } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip96FileStorage/config/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip96FileStorage/config/TagArrayBuilderExt.kt new file mode 100644 index 000000000..7f18c9ed4 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip96FileStorage/config/TagArrayBuilderExt.kt @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip96FileStorage.config + +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip96FileStorage.config.tags.ServerTag + +fun TagArrayBuilder.servers(server: String) = add(ServerTag.assemble(server)) + +fun TagArrayBuilder.servers(servers: List) = addAll(ServerTag.assemble(servers)) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip96FileStorage/config/tags/ServerTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip96FileStorage/config/tags/ServerTag.kt new file mode 100644 index 000000000..4e326bb9d --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip96FileStorage/config/tags/ServerTag.kt @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip96FileStorage.config.tags + +class ServerTag { + companion object { + const val TAG_NAME = "server" + const val TAG_SIZE = 2 + + fun isTag(tag: Array) = tag[0] == TAG_NAME + + @JvmStatic + fun parse(tag: Array): String? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1] + } + + @JvmStatic + fun assemble(url: String) = arrayOf(TAG_NAME, url) + + @JvmStatic + fun assemble(urls: List) = urls.map { assemble(it) } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip96FileStorage/ServerInfo.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip96FileStorage/info/ServerInfo.kt similarity index 97% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip96FileStorage/ServerInfo.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip96FileStorage/info/ServerInfo.kt index 8cccb5df7..0902bbdb6 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip96FileStorage/ServerInfo.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip96FileStorage/info/ServerInfo.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip96FileStorage +package com.vitorpamplona.quartz.nip96FileStorage.info import com.fasterxml.jackson.annotation.JsonProperty diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip96FileStorage/ServerInfoParser.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip96FileStorage/info/ServerInfoParser.kt similarity index 98% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip96FileStorage/ServerInfoParser.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip96FileStorage/info/ServerInfoParser.kt index f88d0122d..0f0dcb647 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip96FileStorage/ServerInfoParser.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip96FileStorage/info/ServerInfoParser.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip96FileStorage +package com.vitorpamplona.quartz.nip96FileStorage.info import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip98HttpAuth/HTTPAuthorizationEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip98HttpAuth/HTTPAuthorizationEvent.kt index e2c39264e..7b09bc39b 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip98HttpAuth/HTTPAuthorizationEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip98HttpAuth/HTTPAuthorizationEvent.kt @@ -21,12 +21,15 @@ package com.vitorpamplona.quartz.nip98HttpAuth import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.CryptoUtils import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip01Core.toHexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip98HttpAuth.tags.MethodTag +import com.vitorpamplona.quartz.nip98HttpAuth.tags.PayloadHashTag +import com.vitorpamplona.quartz.nip98HttpAuth.tags.UrlTag import com.vitorpamplona.quartz.utils.TimeUtils +import java.util.Base64 @Immutable class HTTPAuthorizationEvent( @@ -37,28 +40,30 @@ class HTTPAuthorizationEvent( content: String, sig: HexKey, ) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { + fun method() = tags.firstNotNullOfOrNull(MethodTag::parse) + + fun payloadHash() = tags.firstNotNullOfOrNull(PayloadHashTag::parse) + + fun url() = tags.firstNotNullOfOrNull(UrlTag::parse) + + fun rawToken() = Base64.getEncoder().encodeToString(toJson().toByteArray()) + + fun toAuthToken() = "Nostr ${rawToken()}" + companion object { const val KIND = 27235 - fun create( + fun build( url: String, method: String, file: ByteArray? = null, - signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (HTTPAuthorizationEvent) -> Unit, - ) { - var hash = "" - file?.let { hash = CryptoUtils.sha256(file).toHexKey() } - - val tags = - listOfNotNull( - arrayOf("u", url), - arrayOf("method", method), - arrayOf("payload", hash), - ) - - signer.sign(createdAt, KIND, tags.toTypedArray(), "", onReady) + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, "", createdAt) { + url(url) + method(method) + file?.let { payload(it) } + initializer() } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip98HttpAuth/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip98HttpAuth/TagArrayBuilderExt.kt new file mode 100644 index 000000000..efadeb21d --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip98HttpAuth/TagArrayBuilderExt.kt @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip98HttpAuth + +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip98HttpAuth.tags.MethodTag +import com.vitorpamplona.quartz.nip98HttpAuth.tags.PayloadHashTag +import com.vitorpamplona.quartz.nip98HttpAuth.tags.UrlTag + +fun TagArrayBuilder.url(url: String) = addUnique(UrlTag.assemble(url)) + +fun TagArrayBuilder.method(method: String) = addUnique(MethodTag.assemble(method)) + +fun TagArrayBuilder.payloadHash(hash: String) = addUnique(PayloadHashTag.assemble(hash)) + +fun TagArrayBuilder.payload(bytes: ByteArray) = addUnique(PayloadHashTag.assemble(bytes)) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip98HttpAuth/tags/MethodTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip98HttpAuth/tags/MethodTag.kt new file mode 100644 index 000000000..5459cfdf5 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip98HttpAuth/tags/MethodTag.kt @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip98HttpAuth.tags + +class MethodTag { + companion object { + const val TAG_NAME = "method" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): String? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1] + } + + @JvmStatic + fun assemble(title: String) = arrayOf(TAG_NAME, title) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip98HttpAuth/tags/PayloadHashTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip98HttpAuth/tags/PayloadHashTag.kt new file mode 100644 index 000000000..85904bb3b --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip98HttpAuth/tags/PayloadHashTag.kt @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip98HttpAuth.tags + +import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.toHexKey +import com.vitorpamplona.quartz.utils.sha256Hash + +class PayloadHashTag { + companion object { + const val TAG_NAME = "payload" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): HexKey? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1] + } + + @JvmStatic + fun assemble(hash: HexKey) = arrayOf(TAG_NAME, hash) + + @JvmStatic + fun assemble(payload: ByteArray) = assemble(sha256Hash(payload).toHexKey()) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip98HttpAuth/tags/UrlTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip98HttpAuth/tags/UrlTag.kt new file mode 100644 index 000000000..a8f1afd40 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip98HttpAuth/tags/UrlTag.kt @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip98HttpAuth.tags + +class UrlTag { + companion object { + const val TAG_NAME = "u" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): String? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1] + } + + @JvmStatic + fun assemble(title: String) = arrayOf(TAG_NAME, title) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip99Classifieds/ClassifiedsEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip99Classifieds/ClassifiedsEvent.kt index 41240d58c..7346ebd34 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip99Classifieds/ClassifiedsEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip99Classifieds/ClassifiedsEvent.kt @@ -23,21 +23,22 @@ package com.vitorpamplona.quartz.nip99Classifieds import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag -import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohashMipMap -import com.vitorpamplona.quartz.nip01Core.tags.hashtags.buildHashtagTags -import com.vitorpamplona.quartz.nip10Notes.content.findHashtags -import com.vitorpamplona.quartz.nip10Notes.content.findURLs -import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrl -import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer -import com.vitorpamplona.quartz.nip36SensitiveContent.ContentWarningSerializer -import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetup -import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetupSerializer -import com.vitorpamplona.quartz.nip57Zaps.zapraiser.ZapRaiserSerializer -import com.vitorpamplona.quartz.nip92IMeta.IMetaTag -import com.vitorpamplona.quartz.nip92IMeta.Nip92MediaAttachments +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.core.containsAllTagNamesWithValues +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag +import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags +import com.vitorpamplona.quartz.nip23LongContent.tags.ImageTag +import com.vitorpamplona.quartz.nip23LongContent.tags.PublishedAtTag +import com.vitorpamplona.quartz.nip23LongContent.tags.SummaryTag +import com.vitorpamplona.quartz.nip23LongContent.tags.TitleTag +import com.vitorpamplona.quartz.nip31Alts.alt +import com.vitorpamplona.quartz.nip99Classifieds.tags.ConditionTag +import com.vitorpamplona.quartz.nip99Classifieds.tags.LocationTag +import com.vitorpamplona.quartz.nip99Classifieds.tags.PriceTag +import com.vitorpamplona.quartz.nip99Classifieds.tags.StatusTag import com.vitorpamplona.quartz.utils.TimeUtils +import java.util.UUID @Immutable class ClassifiedsEvent( @@ -48,164 +49,57 @@ class ClassifiedsEvent( content: String, sig: HexKey, ) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) { - fun title() = tags.firstOrNull { it.size > 1 && it[0] == "title" }?.get(1) + fun title() = tags.firstNotNullOfOrNull(TitleTag::parse) - fun image() = tags.firstOrNull { it.size > 1 && it[0] == "image" }?.get(1) + fun image() = tags.firstNotNullOfOrNull(ImageTag::parse) - fun condition() = tags.firstOrNull { it.size > 1 && it[0] == "condition" }?.get(1) + fun condition() = tags.firstNotNullOfOrNull(ConditionTag::parse) - fun images() = tags.filter { it.size > 1 && it[0] == "image" }.map { it[1] } + fun images() = tags.mapNotNull(ImageTag::parse) - fun summary() = tags.firstOrNull { it.size > 1 && it[0] == "summary" }?.get(1) + fun status() = tags.firstNotNullOfOrNull(StatusTag::parse) - fun price() = - tags - .firstOrNull { it.size > 1 && it[0] == "price" } - ?.let { Price(it[1], it.getOrNull(2), it.getOrNull(3)) } + fun summary() = tags.firstNotNullOfOrNull(SummaryTag::parse) - fun location() = tags.firstOrNull { it.size > 1 && it[0] == "location" }?.get(1) + fun price() = tags.firstNotNullOfOrNull(PriceTag::parse) - fun isWellFormed(): Boolean { - var hasImage = false - var hasTitle = false - var hasPrice = false + fun location() = tags.firstNotNullOfOrNull(LocationTag::parse) - tags.forEach { - if (it.size > 1) { - if (it[0] == "image") { - hasImage = true - } else if (it[0] == "title") { - hasTitle = true - } else if (it[0] == "price") { - hasPrice = true - } - } - } + fun publishedAt() = tags.firstNotNullOfOrNull(PublishedAtTag::parse) - return hasImage && hasPrice && hasTitle - } + fun categories() = tags.hashtags() - fun publishedAt() = - try { - tags.firstOrNull { it.size > 1 && it[0] == "published_at" }?.get(1)?.toLongOrNull() - } catch (_: Exception) { - null - } - - enum class CONDITION( - val value: String, - ) { - NEW("new"), - USED_LIKE_NEW("like new"), - USED_GOOD("good"), - USED_FAIR("fair"), - } + fun isWellFormed() = tags.containsAllTagNamesWithValues(REQUIRED_FIELDS) companion object { const val KIND = 30402 - private val imageExtensions = listOf("png", "jpg", "gif", "bmp", "jpeg", "webp", "svg", "avif") - const val ALT = "Classifieds listing" + const val ALT_DESCRIPTION = "Classifieds listing" - fun create( - dTag: String, - title: String?, - image: String?, - summary: String?, - message: String, - price: Price?, - location: String?, - category: String?, - condition: CONDITION?, - publishedAt: Long? = TimeUtils.now(), - replyTos: List?, - addresses: List?, - mentions: List?, - directMentions: Set, - zapReceiver: List? = null, - markAsSensitive: Boolean, - zapRaiserAmount: Long?, - geohash: String? = null, - imetas: List? = null, - emojis: List? = null, - signer: NostrSigner, + val REQUIRED_FIELDS = setOf(TitleTag.TAG_NAME, PriceTag.TAG_NAME, ImageTag.TAG_NAME) + + fun build( + title: String, + price: PriceTag, + description: String, + location: String? = null, + condition: ConditionTag.CONDITION? = null, + images: List? = null, + status: StatusTag.STATUS = StatusTag.STATUS.ACTIVE, + dTag: String = UUID.randomUUID().toString(), createdAt: Long = TimeUtils.now(), - isDraft: Boolean, - onReady: (ClassifiedsEvent) -> Unit, - ) { - val tags = mutableListOf>() + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, description, createdAt) { + dTag(dTag) + title(title) + price(price) + status(status) - replyTos?.forEach { - if (it in directMentions) { - tags.add(arrayOf("e", it, "", "mention")) - } else { - tags.add(arrayOf("e", it)) - } - } - mentions?.forEach { - if (it in directMentions) { - tags.add(arrayOf("p", it, "", "mention")) - } else { - tags.add(arrayOf("p", it)) - } - } - addresses?.forEach { - val aTag = it.toTag() - if (aTag in directMentions) { - tags.add(arrayOf("a", aTag, "", "mention")) - } else { - tags.add(arrayOf("a", aTag)) - } - } + condition?.let { condition(it) } + location?.let { location(it) } + images?.let { images(images) } - tags.add(arrayOf("d", dTag)) - title?.let { tags.add(arrayOf("title", it)) } - image?.let { tags.add(arrayOf("image", it)) } - summary?.let { tags.add(arrayOf("summary", it)) } - price?.let { - if (it.frequency != null && it.currency != null) { - tags.add(arrayOf("price", it.amount, it.currency, it.frequency)) - } else if (it.currency != null) { - tags.add(arrayOf("price", it.amount, it.currency)) - } else { - tags.add(arrayOf("price", it.amount)) - } - } - category?.let { tags.add(arrayOf("t", it)) } - location?.let { tags.add(arrayOf("location", it)) } - publishedAt?.let { tags.add(arrayOf("publishedAt", it.toString())) } - condition?.let { tags.add(arrayOf("condition", it.value)) } - tags.addAll(buildHashtagTags(findHashtags(message))) - zapReceiver?.forEach { tags.add(ZapSplitSetupSerializer.toTagArray(it)) } - zapRaiserAmount?.let { tags.add(ZapRaiserSerializer.toTagArray(it)) } - - findURLs(message).forEach { - val removedParamsFromUrl = - if (it.contains("?")) { - it.split("?")[0].lowercase() - } else if (it.contains("#")) { - it.split("#")[0].lowercase() - } else { - it - } - - if (imageExtensions.any { removedParamsFromUrl.endsWith(it) }) { - tags.add(arrayOf("image", it)) - } - tags.add(arrayOf("r", it)) - } - if (markAsSensitive) { - tags.add(ContentWarningSerializer.toTagArray()) - } - geohash?.let { tags.addAll(geohashMipMap(it)) } - imetas?.forEach { tags.add(Nip92MediaAttachments.createTag(it)) } - emojis?.forEach { tags.add(it.toTagArray()) } - tags.add(AltTagSerializer.toTagArray(ALT)) - - if (isDraft) { - signer.assembleRumor(createdAt, KIND, tags.toTypedArray(), message, onReady) - } else { - signer.sign(createdAt, KIND, tags.toTypedArray(), message, onReady) - } + alt(ALT_DESCRIPTION) + initializer() } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip99Classifieds/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip99Classifieds/TagArrayBuilderExt.kt new file mode 100644 index 000000000..2da2c86da --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip99Classifieds/TagArrayBuilderExt.kt @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip99Classifieds + +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip23LongContent.tags.ImageTag +import com.vitorpamplona.quartz.nip23LongContent.tags.PublishedAtTag +import com.vitorpamplona.quartz.nip23LongContent.tags.SummaryTag +import com.vitorpamplona.quartz.nip23LongContent.tags.TitleTag +import com.vitorpamplona.quartz.nip99Classifieds.tags.ConditionTag +import com.vitorpamplona.quartz.nip99Classifieds.tags.LocationTag +import com.vitorpamplona.quartz.nip99Classifieds.tags.PriceTag +import com.vitorpamplona.quartz.nip99Classifieds.tags.StatusTag + +fun TagArrayBuilder.title(title: String) = addUnique(TitleTag.assemble(title)) + +fun TagArrayBuilder.summary(summary: String) = addUnique(SummaryTag.assemble(summary)) + +fun TagArrayBuilder.location(location: String) = addUnique(LocationTag.assemble(location)) + +fun TagArrayBuilder.image(imageUrl: String) = addUnique(ImageTag.assemble(imageUrl)) + +fun TagArrayBuilder.images(imageUrls: List) = addAll(imageUrls.map { ImageTag.assemble(it) }) + +fun TagArrayBuilder.condition(condition: ConditionTag.CONDITION) = addUnique(condition.toTagArray()) + +fun TagArrayBuilder.status(status: StatusTag.STATUS) = addUnique(status.toTagArray()) + +fun TagArrayBuilder.price(price: PriceTag) = addUnique(price.toTagArray()) + +fun TagArrayBuilder.publishedAt(publishedAt: Long) = addUnique(PublishedAtTag.assemble(publishedAt)) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip99Classifieds/tags/ConditionTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip99Classifieds/tags/ConditionTag.kt new file mode 100644 index 000000000..658a694c6 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip99Classifieds/tags/ConditionTag.kt @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip99Classifieds.tags + +class ConditionTag { + enum class CONDITION( + val value: String, + ) { + NEW("new"), + USED_LIKE_NEW("like new"), + USED_GOOD("good"), + USED_FAIR("fair"), + ; + + fun toTagArray() = assemble(this) + } + + companion object { + const val TAG_NAME = "condition" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): String? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1] + } + + @JvmStatic + fun assemble(condition: CONDITION) = arrayOf(TAG_NAME, condition.value) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip99Classifieds/tags/LocationTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip99Classifieds/tags/LocationTag.kt new file mode 100644 index 000000000..1d8966ef1 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip99Classifieds/tags/LocationTag.kt @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip99Classifieds.tags + +class LocationTag { + companion object { + const val TAG_NAME = "location" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): String? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1] + } + + @JvmStatic + fun assemble(locationName: String) = arrayOf(TAG_NAME, locationName) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip99Classifieds/tags/PriceTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip99Classifieds/tags/PriceTag.kt new file mode 100644 index 000000000..48c2e4801 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip99Classifieds/tags/PriceTag.kt @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip99Classifieds.tags + +import com.vitorpamplona.quartz.utils.arrayOfNotNull + +data class PriceTag( + val amount: String, + val currency: String?, + val frequency: String?, +) { + fun toTagArray() = assemble(amount, currency, frequency) + + companion object { + const val TAG_NAME = "price" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): PriceTag? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return PriceTag(tag[1], tag.getOrNull(2), tag.getOrNull(3)) + } + + @JvmStatic + fun assemble( + amount: String, + currency: String?, + frequency: String?, + ) = arrayOfNotNull(TAG_NAME, amount, currency, frequency) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip99Classifieds/tags/StatusTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip99Classifieds/tags/StatusTag.kt new file mode 100644 index 000000000..d4365b82f --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip99Classifieds/tags/StatusTag.kt @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip99Classifieds.tags + +class StatusTag { + enum class STATUS( + val value: String, + ) { + ACTIVE("active"), + SOLD("sold"), + ; + + fun toTagArray() = assemble(this) + } + + companion object { + const val TAG_NAME = "status" + const val TAG_SIZE = 2 + + @JvmStatic + fun parse(tag: Array): String? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1] + } + + @JvmStatic + fun assemble(status: STATUS) = arrayOf(TAG_NAME, status.value) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/utils/ArrayUtils.kt b/quartz/src/main/java/com/vitorpamplona/quartz/utils/ArrayUtils.kt index f8c891f87..9f1725f88 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/utils/ArrayUtils.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/utils/ArrayUtils.kt @@ -20,6 +20,8 @@ */ package com.vitorpamplona.quartz.utils +import com.vitorpamplona.quartz.R + public fun arrayOfNotNull(vararg elements: String?) = removeTrailingNullsAndEmptyOthers(*elements) public fun removeTrailingNullsAndEmptyOthers(vararg elements: String?): Array { @@ -39,3 +41,13 @@ fun Array.startsWith(startsWith: Array): Boolean { } return true } + +public inline fun Array.lastNotNullOfOrNull(transform: (T) -> R?): R? { + for (index in this.indices.reversed()) { + val result = transform(this[index]) + if (result != null) { + return result + } + } + return null +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/utils/StringUtils.kt b/quartz/src/main/java/com/vitorpamplona/quartz/utils/StringUtils.kt index 085856d6f..8cfb1ce11 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/utils/StringUtils.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/utils/StringUtils.kt @@ -26,6 +26,10 @@ val pointerSizeInBytes = 4 fun String.bytesUsedInMemory(): Long = (8 * (((this.length * 2L) + 45) / 8)) +fun Long.bytesUsedInMemory(): Long = 8 + +fun Int.bytesUsedInMemory(): Long = 4 + fun Boolean.bytesUsedInMemory(): Long = 8 fun String.containsIgnoreCase(term: String): Boolean { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/utils/TimeUtils.kt b/quartz/src/main/java/com/vitorpamplona/quartz/utils/TimeUtils.kt index 02888e38c..8e6341c09 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/utils/TimeUtils.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/utils/TimeUtils.kt @@ -57,5 +57,5 @@ object TimeUtils { fun oneMonthAgo() = now() - ONE_MONTH - fun randomWithTwoDays() = System.currentTimeMillis() / 1000 - CryptoUtils.randomInt(twoDays()) + fun randomWithTwoDays() = now() - CryptoUtils.randomInt(twoDays()) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/utils/UriReferenceExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/utils/UriReferenceExt.kt new file mode 100644 index 000000000..022055ca2 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/utils/UriReferenceExt.kt @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.utils + +import org.czeal.rfc3986.URIReference + +fun URIReference.toStringSchemeHost(): String { + val sb = StringBuilder() + + if (scheme != null) sb.append(scheme).append(":") + if (authority != null) sb.append("//").append(authority.toString()) + + return sb.toString() +} + +fun URIReference.toStringNoFragment(): String { + val sb = StringBuilder() + + if (scheme != null) sb.append(scheme).append(":") + if (authority != null) sb.append("//").append(authority.toString()) + if (path != null) sb.append(path) + if (query != null) sb.append("?").append(query) + + return sb.toString() +} diff --git a/quartz/src/test/java/com/vitorpamplona/quartz/nip96FileStorage/Nip96Test.kt b/quartz/src/test/java/com/vitorpamplona/quartz/nip96FileStorage/Nip96Test.kt index 7e9752278..aaa870e13 100644 --- a/quartz/src/test/java/com/vitorpamplona/quartz/nip96FileStorage/Nip96Test.kt +++ b/quartz/src/test/java/com/vitorpamplona/quartz/nip96FileStorage/Nip96Test.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.quartz.nip96FileStorage +import com.vitorpamplona.quartz.nip96FileStorage.info.ServerInfoParser import junit.framework.TestCase.assertEquals import org.junit.Test diff --git a/quartz/src/test/java/com/vitorpamplona/quartz/utils/HexEncodingTest.kt b/quartz/src/test/java/com/vitorpamplona/quartz/utils/HexEncodingTest.kt new file mode 100644 index 000000000..0ccda6f68 --- /dev/null +++ b/quartz/src/test/java/com/vitorpamplona/quartz/utils/HexEncodingTest.kt @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.utils + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import kotlin.random.Random + +class HexEncodingTest { + val testHex = "48a72b485d38338627ec9d427583551f9af4f016c739b8ec0d6313540a8b12cf" + + @Test + fun testHexEncodeDecodeOurs() { + assertEquals( + testHex, + Hex.encode( + Hex.decode(testHex), + ), + ) + } + + @Test + fun testIsHex() { + assertFalse("/0", Hex.isHex("/0")) + assertFalse("/.", Hex.isHex("/.")) + assertFalse("::", Hex.isHex("::")) + assertFalse("!!", Hex.isHex("!!")) + assertFalse("@@", Hex.isHex("@@")) + assertFalse("GG", Hex.isHex("GG")) + assertFalse("FG", Hex.isHex("FG")) + assertFalse("`a", Hex.isHex("`a")) + assertFalse("gg", Hex.isHex("gg")) + assertFalse("fg", Hex.isHex("fg")) + } + + @OptIn(ExperimentalStdlibApi::class) + @Test + fun testRandomsIsHex() { + for (i in 0..10000) { + val bytes = Random.nextBytes(32) + val hex = bytes.toHexString(HexFormat.Default) + assertTrue(hex, Hex.isHex(hex)) + val hexUpper = bytes.toHexString(HexFormat.UpperCase) + assertTrue(hexUpper, Hex.isHex(hexUpper)) + } + } + + @OptIn(ExperimentalStdlibApi::class) + @Test + fun testRandomsUppercase() { + for (i in 0..1000) { + val bytes = Random.nextBytes(32) + val hex = bytes.toHexString(HexFormat.UpperCase) + assertEquals( + bytes.toList(), + Hex.decode(hex).toList(), + ) + } + } +} diff --git a/quartz/src/test/java/com/vitorpamplona/quartz/utils/TimeUtilsTest.kt b/quartz/src/test/java/com/vitorpamplona/quartz/utils/StringUtilsTest.kt similarity index 99% rename from quartz/src/test/java/com/vitorpamplona/quartz/utils/TimeUtilsTest.kt rename to quartz/src/test/java/com/vitorpamplona/quartz/utils/StringUtilsTest.kt index 19b296238..54d772977 100644 --- a/quartz/src/test/java/com/vitorpamplona/quartz/utils/TimeUtilsTest.kt +++ b/quartz/src/test/java/com/vitorpamplona/quartz/utils/StringUtilsTest.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.quartz.utils import junit.framework.TestCase import org.junit.Test -class TimeUtilsTest { +class StringUtilsTest { private val test = """Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, consectetur, from a Lorem Ipsum passage, and going through the cites of the word in classical literature, discovered the undoubtable source. Lorem Ipsum comes from sections 1.10.32 and 1.10.33 of "de Finibus Bonorum et Malorum" (The Extremes of Good and Evil) by Cicero, written in 45 BC. This book is a treatise on the theory of ethics, very popular during the Renaissance. The first line of Lorem Ipsum, "Lorem ipsum dolor sit amet..", comes from a line in section 1.10.32.