From d825995ef4acbbbb9d03576e50a52a65b6a297a4 Mon Sep 17 00:00:00 2001 From: David Kaspar Date: Tue, 28 Jan 2025 11:39:46 +0100 Subject: [PATCH 01/76] Exclude FileHeaderEvent from note search results (direct note id search and free text) --- .../vitorpamplona/amethyst/model/LocalCache.kt | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) 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 b1e88fb1a7..99ffbf2644 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -1855,7 +1855,17 @@ object LocalCache { if (key != null) { val note = getNoteIfExists(key) - if (note != null) { + if ((note != null) && + !( + note.event is GenericRepostEvent || + note.event is RepostEvent || + note.event is CommunityPostApprovalEvent || + note.event is ReactionEvent || + note.event is LnZapEvent || + note.event is LnZapRequestEvent || + note.event is FileHeaderEvent + ) + ) { return listOfNotNull(note) } } @@ -1866,7 +1876,8 @@ object LocalCache { note.event is CommunityPostApprovalEvent || note.event is ReactionEvent || note.event is LnZapEvent || - note.event is LnZapRequestEvent + note.event is LnZapRequestEvent || + note.event is FileHeaderEvent ) { return@filter false } @@ -1897,7 +1908,8 @@ object LocalCache { addressable.event is CommunityPostApprovalEvent || addressable.event is ReactionEvent || addressable.event is LnZapEvent || - addressable.event is LnZapRequestEvent + addressable.event is LnZapRequestEvent || + addressable.event is FileHeaderEvent ) { return@filter false } From 1bd52f163f996964b237c5872cd1262ca09f8f72 Mon Sep 17 00:00:00 2001 From: David Kaspar Date: Tue, 28 Jan 2025 11:55:17 +0100 Subject: [PATCH 02/76] extract logical expression for which notes to exclude from search results --- .../amethyst/model/LocalCache.kt | 45 ++++++++----------- 1 file changed, 18 insertions(+), 27 deletions(-) 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 99ffbf2644..be90a35c42 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -1845,6 +1845,21 @@ object LocalCache { ) } + /** + * Will return true if supplied note is one of events to be excluded from + * search results. + */ + private fun excludeNoteEventFromSearchResults(note: Note): Boolean = + ( + note.event is GenericRepostEvent || + note.event is RepostEvent || + note.event is CommunityPostApprovalEvent || + note.event is ReactionEvent || + note.event is LnZapEvent || + note.event is LnZapRequestEvent || + note.event is FileHeaderEvent + ) + fun findNotesStartingWith( text: String, forAccount: Account, @@ -1855,30 +1870,13 @@ object LocalCache { if (key != null) { val note = getNoteIfExists(key) - if ((note != null) && - !( - note.event is GenericRepostEvent || - note.event is RepostEvent || - note.event is CommunityPostApprovalEvent || - note.event is ReactionEvent || - note.event is LnZapEvent || - note.event is LnZapRequestEvent || - note.event is FileHeaderEvent - ) - ) { + if ((note != null) && !excludeNoteEventFromSearchResults(note)) { return listOfNotNull(note) } } return notes.filter { _, note -> - if (note.event is GenericRepostEvent || - note.event is RepostEvent || - note.event is CommunityPostApprovalEvent || - note.event is ReactionEvent || - note.event is LnZapEvent || - note.event is LnZapRequestEvent || - note.event is FileHeaderEvent - ) { + if (excludeNoteEventFromSearchResults(note)) { return@filter false } @@ -1903,14 +1901,7 @@ object LocalCache { return@filter false } + addressables.filter { _, addressable -> - if (addressable.event is GenericRepostEvent || - addressable.event is RepostEvent || - addressable.event is CommunityPostApprovalEvent || - addressable.event is ReactionEvent || - addressable.event is LnZapEvent || - addressable.event is LnZapRequestEvent || - addressable.event is FileHeaderEvent - ) { + if (excludeNoteEventFromSearchResults(addressable)) { return@filter false } From 9caf0d28f51cc81a8b6a84ac5195e56a9cd6f255 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Thu, 30 Jan 2025 19:48:11 +0000 Subject: [PATCH 03/76] New Crowdin translations by GitHub Action --- amethyst/src/main/res/values-hu/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/amethyst/src/main/res/values-hu/strings.xml b/amethyst/src/main/res/values-hu/strings.xml index 86612ee458..0f73f77874 100644 --- a/amethyst/src/main/res/values-hu/strings.xml +++ b/amethyst/src/main/res/values-hu/strings.xml @@ -53,7 +53,7 @@ Zap Megtekintések száma Megtolás - Megtolva + megtolta szerkesztve #%1$s szerkesztése eredeti From e9861d80f69d7676976339ce2cf0629db0957373 Mon Sep 17 00:00:00 2001 From: dbth <1097224+believethehype@users.noreply.github.com> Date: Sat, 1 Feb 2025 21:48:26 +0100 Subject: [PATCH 04/76] add option for 2 profile gallery views --- .../vitorpamplona/amethyst/model/Settings.kt | 18 +++++++++++++++++ .../ui/screen/SharedPreferencesViewModel.kt | 19 ++++++++++++++++++ .../profile/gallery/GalleryCardCompose.kt | 7 ++++++- .../loggedIn/profile/gallery/GalleryThumb.kt | 17 ++++++++++------ .../profile/gallery/ProfileGalleryFeed.kt | 13 +++++++++++- .../loggedIn/settings/AppSettingsScreen.kt | 20 +++++++++++++++++++ amethyst/src/main/res/values/strings.xml | 6 ++++++ 7 files changed, 92 insertions(+), 8 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Settings.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Settings.kt index 25798635a2..cc722dc75f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Settings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Settings.kt @@ -35,6 +35,7 @@ data class Settings( val dontShowPushNotificationSelector: Boolean = false, val dontAskForNotificationPermissions: Boolean = false, val featureSet: FeatureSetType = FeatureSetType.SIMPLIFIED, + val gallerySet: ProfileGalleryType = ProfileGalleryType.CLASSIC, ) enum class ThemeType( @@ -75,6 +76,14 @@ enum class FeatureSetType( PERFORMANCE(2, R.string.ui_feature_set_type_performance), } +enum class ProfileGalleryType( + val screenCode: Int, + val resourceId: Int, +) { + CLASSIC(0, R.string.gallery_type_classic), + MODERN(1, R.string.gallery_type_modern), +} + fun parseConnectivityType(code: Boolean?): ConnectivityType = when (code) { ConnectivityType.ALWAYS.prefCode -> ConnectivityType.ALWAYS @@ -105,6 +114,15 @@ fun parseFeatureSetType(screenCode: Int): FeatureSetType = } } +fun parseGalleryType(screenCode: Int): ProfileGalleryType = + when (screenCode) { + ProfileGalleryType.CLASSIC.screenCode -> ProfileGalleryType.CLASSIC + ProfileGalleryType.MODERN.screenCode -> ProfileGalleryType.MODERN + else -> { + ProfileGalleryType.CLASSIC + } + } + enum class BooleanType( val prefCode: Boolean?, val screenCode: Int, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/SharedPreferencesViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/SharedPreferencesViewModel.kt index 10a5f21d18..3294969b95 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/SharedPreferencesViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/SharedPreferencesViewModel.kt @@ -38,6 +38,7 @@ import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.model.BooleanType import com.vitorpamplona.amethyst.model.ConnectivityType import com.vitorpamplona.amethyst.model.FeatureSetType +import com.vitorpamplona.amethyst.model.ProfileGalleryType import com.vitorpamplona.amethyst.model.Settings import com.vitorpamplona.amethyst.model.ThemeType import kotlinx.coroutines.Dispatchers @@ -56,6 +57,7 @@ class SettingsState { var dontShowPushNotificationSelector by mutableStateOf(false) var dontAskForNotificationPermissions by mutableStateOf(false) var featureSet by mutableStateOf(FeatureSetType.SIMPLIFIED) + var gallerySet by mutableStateOf(ProfileGalleryType.CLASSIC) var isOnMobileData: State = mutableStateOf(false) @@ -71,6 +73,14 @@ class SettingsState { } } + val modernGalleryStyle = + derivedStateOf { + when (gallerySet) { + ProfileGalleryType.CLASSIC -> false + ProfileGalleryType.MODERN -> true + } + } + val showUrlPreview = derivedStateOf { when (automaticallyShowUrlPreview) { @@ -117,6 +127,7 @@ class SharedPreferencesViewModel : ViewModel() { sharedPrefs.automaticallyShowProfilePictures = savedSettings.automaticallyShowProfilePictures sharedPrefs.dontShowPushNotificationSelector = savedSettings.dontShowPushNotificationSelector sharedPrefs.dontAskForNotificationPermissions = savedSettings.dontAskForNotificationPermissions + sharedPrefs.gallerySet = savedSettings.gallerySet sharedPrefs.featureSet = savedSettings.featureSet updateLanguageInTheUI() @@ -191,6 +202,13 @@ class SharedPreferencesViewModel : ViewModel() { } } + fun updateGallerySetType(newgalleryType: ProfileGalleryType) { + if (sharedPrefs.gallerySet != newgalleryType) { + sharedPrefs.gallerySet = newgalleryType + saveSharedSettings() + } + } + fun dontShowPushNotificationSelector() { if (sharedPrefs.dontShowPushNotificationSelector == false) { sharedPrefs.dontShowPushNotificationSelector = true @@ -237,6 +255,7 @@ class SharedPreferencesViewModel : ViewModel() { sharedPrefs.dontShowPushNotificationSelector, sharedPrefs.dontAskForNotificationPermissions, sharedPrefs.featureSet, + sharedPrefs.gallerySet, ), ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryCardCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryCardCompose.kt index 981fcebe5c..d815efebdc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryCardCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryCardCompose.kt @@ -46,6 +46,7 @@ fun GalleryCardCompose( modifier: Modifier = Modifier, accountViewModel: AccountViewModel, nav: INav, + ratio: Float = 1.0f, ) { WatchNoteEvent(baseNote = baseNote, accountViewModel = accountViewModel, shortPreview = true) { CheckHiddenFeedWatchBlockAndReport( @@ -74,6 +75,7 @@ fun GalleryCardCompose( modifier = modifier, accountViewModel = accountViewModel, nav = nav, + ratio = ratio, ) } else { RedirectableGalleryCard( @@ -82,6 +84,7 @@ fun GalleryCardCompose( modifier = modifier, accountViewModel = accountViewModel, nav = nav, + ratio = ratio, ) } } @@ -92,6 +95,7 @@ fun GalleryCardCompose( modifier = modifier, accountViewModel = accountViewModel, nav = nav, + ratio = ratio, ) } } @@ -105,6 +109,7 @@ fun RedirectableGalleryCard( modifier: Modifier = Modifier, accountViewModel: AccountViewModel, nav: INav, + ratio: Float = 1.0f, ) { QuickActionGallery(baseNote = galleryNote, accountViewModel = accountViewModel) { showPopup -> ClickableNote( @@ -123,7 +128,7 @@ fun RedirectableGalleryCard( note = galleryNote, accountViewModel = accountViewModel, ) { - GalleryThumbnail(galleryNote, accountViewModel, nav) + GalleryThumbnail(galleryNote, accountViewModel, nav, ratio = ratio) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryThumb.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryThumb.kt index 2e01fc52e9..5e27dec942 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryThumb.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryThumb.kt @@ -77,6 +77,7 @@ fun GalleryThumbnail( baseNote: Note, accountViewModel: AccountViewModel, nav: INav, + ratio: Float = 1.0f, ) { val noteState by baseNote.live().metadata.observeAsState() val noteEvent = noteState?.note?.event ?: return @@ -134,7 +135,7 @@ fun GalleryThumbnail( emptyList() } - InnerRenderGalleryThumb(content, baseNote, accountViewModel) + InnerRenderGalleryThumb(content, baseNote, accountViewModel, ratio) } @Composable @@ -142,9 +143,10 @@ fun InnerRenderGalleryThumb( content: List, note: Note, accountViewModel: AccountViewModel, + ratio: Float = 1.0f, ) { if (content.isNotEmpty()) { - GalleryContentView(content, accountViewModel) + GalleryContentView(content, accountViewModel, ratio = ratio) } else { DisplayGalleryAuthorBanner(note) } @@ -162,16 +164,17 @@ fun DisplayGalleryAuthorBanner(note: Note) { fun GalleryContentView( contentList: List, accountViewModel: AccountViewModel, + ratio: Float = 1.0f, ) { AutoNonlazyGrid(contentList.size) { contentIndex -> when (val content = contentList[contentIndex]) { is MediaUrlImage -> SensitivityWarning(content.contentWarning != null, accountViewModel) { - UrlImageView(content, accountViewModel) + UrlImageView(content, accountViewModel, ratio = ratio) } is MediaUrlVideo -> SensitivityWarning(content.contentWarning != null, accountViewModel) { - UrlVideoView(content, accountViewModel) + UrlVideoView(content, accountViewModel, ratio = ratio) } } } @@ -182,8 +185,9 @@ fun UrlImageView( content: MediaUrlImage, accountViewModel: AccountViewModel, alwayShowImage: Boolean = false, + ratio: Float = 1.0f, ) { - val defaultModifier = Modifier.fillMaxSize().aspectRatio(1f) + val defaultModifier = Modifier.fillMaxSize().aspectRatio(ratio) val showImage = remember { @@ -250,8 +254,9 @@ fun UrlImageView( fun UrlVideoView( content: MediaUrlVideo, accountViewModel: AccountViewModel, + ratio: Float = 1.0f, ) { - val defaultModifier = Modifier.fillMaxSize().aspectRatio(1f) + val defaultModifier = Modifier.fillMaxSize().aspectRatio(ratio) val automaticallyStartPlayback = remember(content) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/ProfileGalleryFeed.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/ProfileGalleryFeed.kt index 9cf067dbad..4ab78a01a6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/ProfileGalleryFeed.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/ProfileGalleryFeed.kt @@ -33,6 +33,7 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty import com.vitorpamplona.amethyst.ui.feeds.FeedError @@ -40,6 +41,7 @@ import com.vitorpamplona.amethyst.ui.feeds.FeedState import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.screen.FeedViewModel +import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.FeedPadding @@ -87,6 +89,14 @@ private fun GalleryFeedLoaded( nav: INav, ) { val items by loaded.feed.collectAsStateWithLifecycle() + val sharedPreferencesViewModel: SharedPreferencesViewModel = viewModel() + + sharedPreferencesViewModel.init() + + var ratio = 1.0f + if (sharedPreferencesViewModel.sharedPrefs.modernGalleryStyle.value) { + ratio = 0.8f + } LazyVerticalGrid( columns = GridCells.Fixed(3), @@ -100,11 +110,12 @@ private fun GalleryFeedLoaded( baseNote = item, modifier = Modifier - .aspectRatio(1f) + .aspectRatio(ratio) .fillMaxSize() .animateItem(), accountViewModel = accountViewModel, nav = nav, + ratio = ratio, ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt index 45e4e4ceae..efcb0a3edb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt @@ -48,10 +48,12 @@ import androidx.core.os.LocaleListCompat import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.ConnectivityType import com.vitorpamplona.amethyst.model.FeatureSetType +import com.vitorpamplona.amethyst.model.ProfileGalleryType import com.vitorpamplona.amethyst.model.ThemeType import com.vitorpamplona.amethyst.model.parseBooleanType import com.vitorpamplona.amethyst.model.parseConnectivityType import com.vitorpamplona.amethyst.model.parseFeatureSetType +import com.vitorpamplona.amethyst.model.parseGalleryType import com.vitorpamplona.amethyst.model.parseThemeType import com.vitorpamplona.amethyst.ui.components.PushNotificationSettingsRow import com.vitorpamplona.amethyst.ui.navigation.INav @@ -187,6 +189,12 @@ fun SettingsScreen(sharedPreferencesViewModel: SharedPreferencesViewModel) { TitleExplainer(stringRes(FeatureSetType.PERFORMANCE.resourceId)), ) + val galleryItems = + persistentListOf( + TitleExplainer(stringRes(ProfileGalleryType.CLASSIC.resourceId)), + TitleExplainer(stringRes(ProfileGalleryType.MODERN.resourceId)), + ) + val showImagesIndex = sharedPreferencesViewModel.sharedPrefs.automaticallyShowImages.screenCode val videoIndex = sharedPreferencesViewModel.sharedPrefs.automaticallyStartPlayback.screenCode val linkIndex = sharedPreferencesViewModel.sharedPrefs.automaticallyShowUrlPreview.screenCode @@ -204,6 +212,8 @@ fun SettingsScreen(sharedPreferencesViewModel: SharedPreferencesViewModel) { val featureSetIndex = sharedPreferencesViewModel.sharedPrefs.featureSet.screenCode + val galleryIndex = + sharedPreferencesViewModel.sharedPrefs.gallerySet.screenCode Column( Modifier @@ -295,9 +305,19 @@ fun SettingsScreen(sharedPreferencesViewModel: SharedPreferencesViewModel) { ) { sharedPreferencesViewModel.updateFeatureSetType(parseFeatureSetType(it)) } + Spacer(modifier = HalfVertSpacer) Spacer(modifier = HalfVertSpacer) + SettingsRow( + R.string.gallery_style, + R.string.gallery_style_description, + galleryItems, + galleryIndex, + ) { + sharedPreferencesViewModel.updateGallerySetType(parseGalleryType(it)) + } + PushNotificationSettingsRow(sharedPreferencesViewModel) } } diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index c779f88a8f..6f14f96620 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -634,6 +634,9 @@ Simplified Performance + Classic + Modern + System Light Dark @@ -650,6 +653,9 @@ UI Mode Choose the post style + Profile Gallery Style + Choose the gallery style + Load Image Spammers From 2eece49b9b1ecb92e1488472c4b0e47df6b54eaa Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Mon, 10 Feb 2025 13:43:23 +0000 Subject: [PATCH 05/76] New Crowdin translations by GitHub Action --- amethyst/src/main/res/values-bn-rBD/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/amethyst/src/main/res/values-bn-rBD/strings.xml b/amethyst/src/main/res/values-bn-rBD/strings.xml index 731d0bb496..afaf5ef7fd 100644 --- a/amethyst/src/main/res/values-bn-rBD/strings.xml +++ b/amethyst/src/main/res/values-bn-rBD/strings.xml @@ -104,6 +104,7 @@ ওয়েবসাইটের URL বিজলি-ঠিকানা বিজলি URL (অপ্রচলিত) + গ্যালারিতে সংরক্ষণ করুন ছবিটি গ্যালারিতে সংরক্ষণ করা হয়েছে ছবিটি সফলভাবে সংরক্ষণ করা যায় নি ছবিটি আপলোড করুন From 68d46864242312280d9c1a9be5194304d0e7fd17 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 13 Feb 2025 17:10:51 -0500 Subject: [PATCH 06/76] Adds NostrMedia as a paid blossom relay option --- .../vitorpamplona/amethyst/ui/actions/mediaServers/ServerName.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/ServerName.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/ServerName.kt index 9574f0c5c0..d22297eed9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/ServerName.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/ServerName.kt @@ -41,4 +41,5 @@ val DEFAULT_MEDIA_SERVERS: List = ServerName("Satellite (Paid)", "https://cdn.satellite.earth", ServerType.Blossom), ServerName("NostrCheck.me (Blossom)", "https://cdn.nostrcheck.me", ServerType.Blossom), ServerName("Nostr.Download", "https://nostr.download", ServerType.Blossom), + ServerName("NostrMedia (Paid)", "https://nostrmedia.com", ServerType.Blossom), ) From 0f3b3c4b2d7f6dfc9704666ddc6265d36ed42431 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 13 Feb 2025 17:19:09 -0500 Subject: [PATCH 07/76] Adds support for NIP-73 --- .../quartz/nip73ExternalIds/BookId.kt | 39 +++++++++++++++++ .../quartz/nip73ExternalIds/ExternalId.kt | 29 +++++++++++++ .../quartz/nip73ExternalIds/GeohashId.kt | 38 +++++++++++++++++ .../quartz/nip73ExternalIds/HashtagId.kt | 38 +++++++++++++++++ .../quartz/nip73ExternalIds/MovieId.kt | 41 ++++++++++++++++++ .../quartz/nip73ExternalIds/PaperId.kt | 39 +++++++++++++++++ .../nip73ExternalIds/PodcastEpisodeId.kt | 39 +++++++++++++++++ .../quartz/nip73ExternalIds/PodcastFeedId.kt | 39 +++++++++++++++++ .../nip73ExternalIds/PodcastPublisherId.kt | 39 +++++++++++++++++ .../quartz/nip73ExternalIds/UrlId.kt | 42 +++++++++++++++++++ 10 files changed, 383 insertions(+) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip73ExternalIds/BookId.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip73ExternalIds/ExternalId.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip73ExternalIds/GeohashId.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip73ExternalIds/HashtagId.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip73ExternalIds/MovieId.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip73ExternalIds/PaperId.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip73ExternalIds/PodcastEpisodeId.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip73ExternalIds/PodcastFeedId.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip73ExternalIds/PodcastPublisherId.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip73ExternalIds/UrlId.kt diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip73ExternalIds/BookId.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip73ExternalIds/BookId.kt new file mode 100644 index 0000000000..812f789587 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip73ExternalIds/BookId.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.nip73ExternalIds + +class BookId( + val isbn: String, + val hint: String? = null, +) : ExternalId { + override fun toScope() = toScope(isbn) + + override fun toKind() = toKind(isbn) + + override fun hint() = hint + + companion object { + // "isbn:9780765382030" + fun toScope(isbn: String) = "isbn:" + isbn.lowercase().replace("-", "") + + fun toKind(isbn: String) = "isbn" + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip73ExternalIds/ExternalId.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip73ExternalIds/ExternalId.kt new file mode 100644 index 0000000000..83acbeca67 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip73ExternalIds/ExternalId.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.nip73ExternalIds + +interface ExternalId { + fun toScope(): String + + fun toKind(): String + + fun hint(): String? +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip73ExternalIds/GeohashId.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip73ExternalIds/GeohashId.kt new file mode 100644 index 0000000000..94c3d1ee5b --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip73ExternalIds/GeohashId.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.nip73ExternalIds + +class GeohashId( + val geohash: String, + val hint: String? = null, +) : ExternalId { + override fun toScope() = toScope(geohash) + + override fun toKind() = toKind(geohash) + + override fun hint() = hint + + companion object { + fun toScope(geohash: String) = "geo:" + geohash.lowercase() + + fun toKind(geohash: String) = "geo" + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip73ExternalIds/HashtagId.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip73ExternalIds/HashtagId.kt new file mode 100644 index 0000000000..7ce563364c --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip73ExternalIds/HashtagId.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.nip73ExternalIds + +class HashtagId( + val topic: String, + val hint: String? = null, +) : ExternalId { + override fun toScope() = toScope(topic) + + override fun toKind() = toKind(topic) + + override fun hint() = hint + + companion object { + fun toScope(topic: String) = "#" + topic.lowercase() + + fun toKind(topic: String) = "#" + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip73ExternalIds/MovieId.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip73ExternalIds/MovieId.kt new file mode 100644 index 0000000000..be8e316fce --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip73ExternalIds/MovieId.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.nip73ExternalIds + +import kotlin.math.min + +class MovieId( + val isan: String, + val hint: String? = null, +) : ExternalId { + override fun toScope() = toScope(isan) + + override fun toKind() = toKind(isan) + + override fun hint() = hint + + companion object { + // "isan:0000-0000-401A-0000-7" + fun toScope(isan: String) = "isan:" + isan.lowercase().substring(0, min(21, isan.length)) + + fun toKind(isan: String) = "isan" + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip73ExternalIds/PaperId.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip73ExternalIds/PaperId.kt new file mode 100644 index 0000000000..1a47e57491 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip73ExternalIds/PaperId.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.nip73ExternalIds + +class PaperId( + val doi: String, + val hint: String? = null, +) : ExternalId { + override fun toScope() = toScope(doi) + + override fun toKind() = toKind(doi) + + override fun hint() = hint + + companion object { + // "doi:10.1000/182" + fun toScope(doi: String) = "doi:" + doi.lowercase() + + fun toKind(doi: String) = "doi" + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip73ExternalIds/PodcastEpisodeId.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip73ExternalIds/PodcastEpisodeId.kt new file mode 100644 index 0000000000..7ce70ae29b --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip73ExternalIds/PodcastEpisodeId.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.nip73ExternalIds + +class PodcastEpisodeId( + val guid: String, + val hint: String? = null, +) : ExternalId { + override fun toScope() = toScope(guid) + + override fun toKind() = toKind(guid) + + override fun hint() = hint + + companion object { + // "isbn:9780765382030" + fun toScope(guid: String) = "podcast:item:guid:" + guid + + fun toKind(guid: String) = "podcast:item:guid" + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip73ExternalIds/PodcastFeedId.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip73ExternalIds/PodcastFeedId.kt new file mode 100644 index 0000000000..f1fe33442d --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip73ExternalIds/PodcastFeedId.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.nip73ExternalIds + +class PodcastFeedId( + val guid: String, + val hint: String? = null, +) : ExternalId { + override fun toScope() = toScope(guid) + + override fun toKind() = toKind(guid) + + override fun hint() = hint + + companion object { + // "isbn:9780765382030" + fun toScope(guid: String) = "podcast:guid:" + guid + + fun toKind(guid: String) = "podcast:guid" + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip73ExternalIds/PodcastPublisherId.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip73ExternalIds/PodcastPublisherId.kt new file mode 100644 index 0000000000..77d7fbbb7b --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip73ExternalIds/PodcastPublisherId.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.nip73ExternalIds + +class PodcastPublisherId( + val guid: String, + val hint: String? = null, +) : ExternalId { + override fun toScope() = toScope(guid) + + override fun toKind() = toKind(guid) + + override fun hint() = hint + + companion object { + // "isbn:9780765382030" + fun toScope(guid: String) = "podcast:publisher:guid:" + guid + + fun toKind(guid: String) = "podcast:publisher:guid" + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip73ExternalIds/UrlId.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip73ExternalIds/UrlId.kt new file mode 100644 index 0000000000..996256fa87 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip73ExternalIds/UrlId.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.nip73ExternalIds + +import com.vitorpamplona.quartz.utils.toStringNoFragment +import com.vitorpamplona.quartz.utils.toStringSchemeHost +import org.czeal.rfc3986.URIReference + +class UrlId( + val url: String, + val hint: String? = null, +) : ExternalId { + override fun toScope() = toScope(url) + + override fun toKind() = toKind(url) + + override fun hint() = hint + + companion object { + fun toScope(url: String) = URIReference.parse(url).normalize().toStringNoFragment() + + fun toKind(url: String) = URIReference.parse(url).normalize().toStringSchemeHost() + } +} From 9c21a8a7da923f0bfd4a9fe117b1dcbcd1e31329 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 13 Feb 2025 17:39:13 -0500 Subject: [PATCH 08/76] 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 fe7f8b9174..e8aaca5677 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 915268552b..956ae15ad9 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 89ad1d1423..c45397357c 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 8babb8e310..cdbab488b1 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 01092dcf1d..20d697e8b6 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 7882749ac5..514a8d696a 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 7d20c24da6..25091ccb32 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 b300173e3e..d6091f0a19 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 c20c9c0ab4..a3fb08daa8 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 3ad4b30d16..09f4b95270 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 b1e88fb1a7..0ca995c3a9 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 152031f671..3d370479c8 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 0284c1704d..b89437a33a 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 615c7e1d5b..a07f3d3c52 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 ff5312ab79..49185405b5 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 1d744cea09..7032f87614 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 ec7320cbb8..310b9d14f5 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 0d2ca1d9e6..2fd76043ae 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 5e8f13eb07..8ddd310fa5 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 e57b4c3f98..a7c5ce4513 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 0e14b62bfe..6fbad76eca 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 413d9a4f7d..dc8f1b73d6 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 9f81afd36a..6e238a4574 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 ca6527a1fa..a6c0d40663 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 b38c6ab1b4..29b45f65a1 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 39bbf93555..93894a463f 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 1cc9266f9c..a62e423ddb 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 d79108c8ab..8e0f28c1bc 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 9a7a9e0335..f07dcfd243 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 7fbb2f3869..f0a0fa46c4 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 252f90d349..b089043fbe 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 bf6b5990a5..991915cf5f 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 07aa7e2a5d..85447bfe17 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 190bb64eab..cd0fa1770a 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 a3aae079aa..85f418a1ba 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 57c2fb6a7b..f2f48552d3 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 954e9b3628..353e5d3cd0 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 3ef79122d5..092bcdc22f 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 a3a3a7dc04..39a35280e3 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 16da96e91f..27ee4b75e9 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 b5d41763c8..bbb09c70fa 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 7d1ad2e080..1ccc4b3a09 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 b100bf1fb1..d95119a312 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 6d6902cd32..ed4f4bee3a 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 a9f56d0f62..aac0d72968 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 25b42c4fe7..e8278ce56e 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 f818db786d..2f096d38e3 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 3f613da331..41816ec2a6 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 8c8a61c203..0f146f7b75 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 d92db895f2..ac3fa4b892 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 9ab6e2b1e2..e1f4806624 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 0263895f65..feff3bacc2 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 da22b4b768..24549b4165 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 11fcdbde40..f7dacd37a9 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 85ddb78cfe..0f850e99f8 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 46f940903a..c0e5233bbc 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 39b17364da..3b33cba4cd 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 42ef9d7bcf..11eb81bd46 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 423ea160f4..0fbc34067c 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 5f3c32e11f..0cd971f287 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 e786fed2ca..2f31da4e89 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 d7ce17ee50..6c51042162 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 6293aeb4e8..b1aa0119e0 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 8a7073c22f..58c139aae9 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 1d55278a06..4ebefbe733 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 39d42849dc..6c8e6561de 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 0a26b7b320..f9d25420b1 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 6693745b3c..e5584738e9 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 c31137858e..85c56cba2a 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 19b13c8800..da10b3858c 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 b93515027a..49e3e57153 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 92ad7b3c74..c72513d8e0 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 2626e695db..d8db702760 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 f4678e0562..e057bc091e 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 87661230aa..f1c079d0b5 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 4a41b1e7fa..3333f97164 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 9864579500..a6b515f5bb 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 18d10f1ce5..66ca7fd2fc 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 19c6e1e62d..6e56760647 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 1c73d589a1..565b2ba64d 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 e64e4c7013..f922b7cadb 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 5074e8fd56..2ffe22d511 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 4dff1bd048..04d59482be 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 f244233b1d..78f5b10e0c 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 4d5e08d346..49d88fbacd 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 02dd65b5fb..7aec26e140 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 518f766cae..d919e88aec 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 942278a7ad..f9476e789d 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 af5e11a53f..7965c62c79 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 aae7ebd3b7..a6e229c3f0 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 2aba7ad5cb..f157feeeee 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 0e3fbc5f07..2a597c8c4b 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 3f29bca70b..4ea7dafb79 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 e8879681e3..f1c4df4ae3 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 46c4344841..64765a8630 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 4a6376a248..54af94aca0 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 907ff96129..3ee743da6a 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 704dd48a2a..3535caff28 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 ae2b1dd4a7..366bd79469 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 0f2a5eff6d..b88b973bb7 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 9e3c1705e4..054b6b8913 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 c60a99b211..b621366692 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 528d391883..9b27beb675 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 41cbe7dd63..239ae46564 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 11410be709..cb69b0d895 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 b608b19033..dca60e57e0 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 fcab0a63fa..03e3304ca5 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 eaa07279ba..b0f0c9c963 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 d5e1a5beb5..5237d3a039 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 98a9dfed51..6efc13e206 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 91e3f9e0fe..472d06f640 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 d78900ec20..0da9a10ac7 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 555b2eae34..090ce4d945 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 da83105547..b217cce080 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 6cbc97a19c..df646155b5 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 05517f21e4..b67dcf95ea 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 6157de2b6e..4346317530 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 ebdbd62a7f..cef42b915d 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 c2902ac3db..2fbcefca73 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 28dcb15228..e7444f8bf1 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 6259245d58..882053d595 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 57c7734785..f7556eee37 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 5e2412e03b..ee0ba65c80 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 24de4b2282..f63c4c79ba 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 a1d9026e6f..c6bfd6c39d 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 d97bb5eddf..7d246191ca 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 a3d440fbae..f4c7d4bfba 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 9eac03605d..bf4295c85e 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 9951132943..a2d4e5ab65 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 78e7a1f675..344e39f11d 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 7120cff011..7f89146749 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 6b441473e0..30737d867b 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 e1f2019d9b..e77750fca3 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 9ba6e2fa53..f21abbea74 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 c99bc0d748..d7dd79387a 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 aa9ac23ac7..0f9e94663e 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 150691d954..b6ea64b987 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 fb71eb80f7..988eb7096c 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 989050479d..7d360ad6f6 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 d9cd9d07d2..c067946cdb 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 c69ee81a10..e0414782a6 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 66a570486b..3ea04e700d 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 66a570486b..3ea04e700d 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 2e780a1344..48dcf6594e 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 66a570486b..3ea04e700d 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 66a570486b..3ea04e700d 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 66a570486b..3ea04e700d 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 66a570486b..3ea04e700d 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 66a570486b..3ea04e700d 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 66a570486b..3ea04e700d 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 66a570486b..3ea04e700d 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 66a570486b..3ea04e700d 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 66a570486b..3ea04e700d 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 66a570486b..3ea04e700d 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 66a570486b..3ea04e700d 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 66a570486b..3ea04e700d 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 25197aa216..4e434e5ce6 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 66a570486b..3ea04e700d 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 0c9d8ae64a..0cd4766e16 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 66a570486b..3ea04e700d 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 d374dcabd9..2392d4a794 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 66a570486b..3ea04e700d 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 84431be216..0335f81304 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 e683d8d8bc..19d667c7c1 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 a0f681e057..70b61d5ccc 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 ab513bc8ec..f585f438f3 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 64007a254b..2a6b27ad2c 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 b228b74f58..303940f938 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 bd2f728db7..9c91ea374d 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 815d6bf249..f246b02195 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 99d0fd4f8b..1f6c577878 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 486e61b82c..a1ba84ae32 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 1a2b38456e..b46402730b 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 2f35046d0b..a29c3ace4f 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 495bae1029..c3921abce0 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 9911848b4e..f50b6db627 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 f842bf2d08..024436e105 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 42bd2aed6f..d1fc02a318 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 f7cbb9306e..5c1183c4e1 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 15255f8cd0..94f03e8201 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 c7b4584b7e..a3325657ba 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 ababe2c5d1..2b5b4862b3 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 a80729109e..d86e208e23 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 e4fa35b219..ce34592c76 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 05151a22f8..7aa3095f84 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\":\"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\",\"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 7a5ef8d5f3..d024ca17ac 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 34ba5aaebb..749d5028e8 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 0000000000..3a8888f004 --- /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 746196fb44..c755059871 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 747271afce..288ec59360 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 21640fde33..af9a55f990 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 29e867baaf..f603c98d35 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 b7968c0f71..2050af360f 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 d8a96b0b3a..e872503a55 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 25a697ebb4..9d4d50e3c6 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 08dab6a150..968ba94fa2 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 78d830d3c1..08b3010312 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 0c0e265d86..822a634aa5 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 6326bd2536..0000000000 --- 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 0000000000..d012db4162 --- /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 0000000000..78280af77a --- /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 0000000000..5f335b67bd --- /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 0000000000..d3bc35de7c --- /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 0000000000..0942299f24 --- /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 25c04be6c6..d7eccfad52 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 0000000000..027b02c9a7 --- /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 0000000000..e52b81d2e3 --- /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 0000000000..d620dbe45f --- /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 0000000000..155f0d9195 --- /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 0000000000..c0439e8dfd --- /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 0000000000..fd3a227c62 --- /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 0000000000..a6ae257b0b --- /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 94a4a603d9..1b1e37669c 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 0000000000..e8f3acd97e --- /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 0000000000..c659d42928 --- /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 e15f7cfa21..74b069dc47 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 116b01825f..79dad5979c 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 0000000000..45c97f641e --- /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 0000000000..9a3217b0e8 --- /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 b09f6fb999..9c3822ab78 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 7446884a4b..96518fcb3a 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 3cbffc1cba..df6f3d15a5 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 1002f5ca70..c81c9ab4ea 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 3b2b46d9fc..4b3a8cff89 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 0000000000..0028687866 --- /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 0000000000..c09d4e7683 --- /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 0000000000..6a14f8e783 --- /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 0000000000..b4a5fabc93 --- /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 efe076e091..f3c829d9f1 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 abac112599..c03618f33d 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 b21bdd6f86..0000000000 --- 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 9bcb44ad04..01af752490 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 0000000000..d7f082826d --- /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 0000000000..9b8a5b71a7 --- /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 0000000000..69911a409a --- /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 dac617f416..1fc8486f6f 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 0000000000..6411d24bb3 --- /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 0000000000..c8f7ded139 --- /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 0000000000..07086b2743 --- /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 0000000000..2eca2f76cc --- /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 69a8c389bc..6eace1de7f 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 f0f36824af..aa9725a8f9 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 0000000000..8023ba0c5b --- /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 90a24d87c0..f4e70bf4fd 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 0000000000..3d371ef77d --- /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 0000000000..df1ccfbfcf --- /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 0000000000..3525445d0b --- /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 0000000000..8a7696c295 --- /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 63fe66daf2..f9ad4057c3 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 0000000000..37b784d2af --- /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 0000000000..5b07019a84 --- /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 0000000000..87e3967ec9 --- /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 0000000000..bf6381dee6 --- /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 0000000000..7f1b8cfe84 --- /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 0000000000..f5a74b06da --- /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 e77cacf121..e2e85884fe 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 af8cda51ee..0f875e2df4 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 c0d1ab8b23..d57e5280ec 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 dd805b2c37..04d0a2d45c 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 0968f210e4..9535c08bb9 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 60920971de..cb2c863e66 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 1c899002ac..66d87fc14c 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 0000000000..71e22c6571 --- /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 aa665d136e..77f49af17d 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 1c0188f812..3d03f667bf 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 f90e576073..a5ac5c4aca 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 3e2f0cb019..6466d3c754 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 fe6090dd7c..f3a41e3fdc 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 f62f9cc68c..dec6165214 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 6fe44d511e..10c8513df0 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 c63fa52dba..fa7870141d 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 caf9fb9251..49ea625d31 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 12c2477dd2..ecbe9a21f8 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 5a83bc4af8..3f06060b2a 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 b1d4252c0c..3f2346bdde 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 ff45dda0cb..c1bff67b6c 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 12aae77f7b..8cf16f8078 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 fcc100b9b7..455090480d 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 2f5f60f874..f4957abd34 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 a025f55286..64dcfcc735 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 7ad6a35b66..eff9c10a5e 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 06ece97638..30ace93e76 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 bbcdbea580..0d494bf1d4 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 4abf3f552f..ea654aebae 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 a9451858c1..fd35bb14df 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 ba44bff653..c2f5d08ac1 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 b68b114bf0..50c4c7d8f4 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 36a8118085..b0ea5d1e0b 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 54f33ae700..002e170284 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 2e703d8605..7d93b978f7 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 ba45a5020b..b90ad14e48 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 7bd48bd03d..ec7864c089 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 08260a6d94..fd5b1a1052 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 7a0bbb43c5..c354ec4d48 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 145c8798ad..830a0eb865 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 9fa6598bb8..36387e350c 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 69dadfb1d6..8d6499d2ae 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 e8fd6960ee..a5c11a1672 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 3a4a50f14d..1cbcdddd99 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 0000000000..60d7527d2a --- /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 d359c1f46c..bfda070b00 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 b56aff3206..70da4e2f41 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 0d633cd4ec..aabdafd1c6 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 2752f19142..a6decad3f5 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 0000000000..9d8c64ffb6 --- /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 0000000000..0591f2ac37 --- /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 0000000000..c5d5046741 --- /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 0000000000..d59535ad3a --- /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 00da4d1d53..fe2fd46f01 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 96fc5e73bb..a85f6be7bc 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 10d45bd70a..c4c0b08fd2 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 0000000000..4196cd21eb --- /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 0000000000..b8af54f12a --- /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 0000000000..260d464e15 --- /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 5010c44c2e..b0ed27872a 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 d4006c78af..650351ccd7 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 0000000000..660315e321 --- /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 a5aef9f3ff..f4be576597 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 0000000000..68e46b97ee --- /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 f2c124274f..ec68e5877e 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 4509f28415..edcfc25620 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 0000000000..ff106533b6 --- /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 0000000000..0511994645 --- /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 0000000000..913f096f5c --- /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 0000000000..ac6c053805 --- /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 0000000000..d44a1e696a --- /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 a573de7737..6248f93539 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 0000000000..3228a7b0a1 --- /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 f04756dd10..4fb4bdb6dc 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 0000000000..8e3e7c9ac4 --- /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 c21f06b529..e8d099ba9f 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 0000000000..a14d9296e8 --- /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 0000000000..a601678077 --- /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 d0a0d6de3f..ae58cf4cac 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 0000000000..c7c74d35a2 --- /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 293170f69f..5364cb0936 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 0000000000..14b46d196c --- /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 0000000000..513481ad0c --- /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 0000000000..f25233d448 --- /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 bfac829eca..3d55455b92 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 0000000000..384c46e599 --- /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 0000000000..625e993dbf --- /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 0000000000..96fbfe9221 --- /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 de2fa6a48a..f02b44eebc 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 84bb7302be..6f73e3195f 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 0000000000..c5a018155e --- /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 2bf2853818..7eeb1be6d0 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 65a3b0bbe3..310d67085a 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 8987c7909b..00bfe41301 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 8c3f1fbb7c..0af01dee06 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 0000000000..384561087b --- /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 0000000000..b4ddaa5e11 --- /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 0000000000..7b0947b386 --- /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 0000000000..95f653af69 --- /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 0000000000..75d0088f27 --- /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 c56efad21e..fd372c1226 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 7213dd466a..5713dd502b 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 a89aa29987..e0ffb08256 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 5baf21f0a6..8489b16693 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 bae13bc22e..816eebc084 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 4e7f538c8f..0300d80fe6 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 003a403b3e..0000000000 --- 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 dceb8aede3..0000000000 --- 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 a85a05c759..ac156bb894 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 0000000000..8ec33b140c --- /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 fbd6d98d8c..4d6da3fe0b 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 1d5ddf6351..ad92949f3f 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 e49b925eec..a422a3ee08 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 0000000000..35fda32418 --- /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 0000000000..4f003cffac --- /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 84572f733b..8f3c7541f9 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 6a90da014b..0931f0396a 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 0000000000..a44727038b --- /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 0000000000..4b57b1bf99 --- /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 0000000000..41c051ad15 --- /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 0000000000..38620fd2d5 --- /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 db83d02c95..680d0ab30c 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 0000000000..c81a70808c --- /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 8b09934216..b6177171ac 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 124d678b0f..08e331d574 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 332ab407b8..db25dc36d4 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 0000000000..eb2d964f51 --- /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 0000000000..2f7169c7e4 --- /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 0000000000..f4ddd5bd99 --- /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 0000000000..864eaf14a1 --- /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 0000000000..f600ecf2e1 --- /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 0000000000..0017af7e01 --- /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 0000000000..bd0d2c1935 --- /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 2ad1577843..67f471930e 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 6d24c4c1a3..1222d013f5 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 c4ceb3bf30..c31746b683 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 5e7d3e93e1..25909ec22f 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 1f56efc181..bc62dec532 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 0000000000..a5b5d2ebe4 --- /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 0000000000..b80a558bf5 --- /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 0000000000..0b42d207b3 --- /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 0000000000..26926372fb --- /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 0000000000..9926fa8ff1 --- /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 0000000000..f3a0028ed3 --- /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 0000000000..2d035639f3 --- /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 0000000000..5b6d44c6f4 --- /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 0000000000..c26282cc4f --- /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 0000000000..34aa37a9d6 --- /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 0000000000..bbbe5861d5 --- /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 38dc910b98..fe75d90341 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 0000000000..bcbbf782c1 --- /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 0000000000..d7e74286ea --- /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 0000000000..671348f27b --- /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 0000000000..1e04d3b56f --- /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 0000000000..409c4eb1d2 --- /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 d4abf954ca..2b7ef491f3 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 717f792934..0000000000 --- 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 4eef28c7d3..14c89f45cb 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 9b8d8ccb88..0000000000 --- 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 137d39136f..0000000000 --- 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 9a223590bb..2779fa7112 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 27fa3e37ce..d8b2b2ad25 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 0000000000..a34f2d39aa --- /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 d5b39ce35d..66b051ed5a 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 0000000000..8857508ed7 --- /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 0000000000..ea1139c71a --- /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 0000000000..170f3d5411 --- /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 0000000000..71a5e61e19 --- /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 0000000000..1d872a0e3c --- /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 d8fb3323a9..8cf13edce8 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 0000000000..b3dec68028 --- /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 bf48b06efb..dee767b073 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 0000000000..9a5067f5e8 --- /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 0000000000..fe638929f2 --- /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 1c24f0314f..e0daf3ac68 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 0000000000..aeacfc2c2f --- /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 0000000000..6a702f12a1 --- /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 0000000000..b28d374655 --- /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 c83d28845b..b23763ad48 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 0000000000..87bb2b59b6 --- /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 a696f58138..a9326360a2 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 74ce3b189d..0000000000 --- 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 7fb1977c6f..8f441aebd1 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 0000000000..7a99b82fda --- /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 befa100b76..5f7a6cbf3b 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 0000000000..d8f6e1ba10 --- /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 0000000000..a3634c4105 --- /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 090121e352..857b95be0d 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 0000000000..ffd653f6b2 --- /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 0000000000..47b1e1f18c --- /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 0000000000..4256d7efea --- /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 0000000000..8fc6aff04a --- /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 0000000000..5c5445856f --- /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 0000000000..81fb74c6a2 --- /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 0000000000..bca45954c3 --- /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 9c022b1509..fad34983c4 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 5b201d6b91..cf5c42caa4 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 0000000000..85697ae112 --- /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 0000000000..895eb423cf --- /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 0000000000..744aad94d6 --- /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 0000000000..8282c36700 --- /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 0000000000..593bad3a70 --- /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 1568449dc0..1262f7e76a 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 0000000000..184b8b6bdd --- /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 0000000000..dc804d02ea --- /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 a3576c6f89..c75bc31405 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 5226596245..0bda989a46 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 c7018bac69..03bb5fff37 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 50a507b111..eff4ce1183 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 e9d32c51fc..bf4b55b548 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 97850dd332..854250f2ec 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 4c7af5e9aa..3c21b34897 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 c84f395f3f..1e17ada6ab 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 2b14d47854..2fad460c85 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 1d4554054c..07920511cd 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 b758d1c589..23d91f8899 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 0000000000..78a72d1f8f --- /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 c1773f5920..8098258531 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 6b7633e593..1540c10480 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 85285b0a1f..5dbe05ff71 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 f8da08bbf0..59166d9110 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 06ac817ddd..2ccfca7928 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 e07f9ba264..77d97fa421 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 8f9cd7f1e3..0bcfd8bcca 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 cd612d77f2..0be0ed45ec 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 9f716ed77b..887f8bbdf8 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 cc2c5468da..0000000000 --- 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 3469e9ff5c..0000000000 --- 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 0000000000..c1ab8889d0 --- /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 0000000000..dc1ea675c3 --- /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 0000000000..19dd12bf1d --- /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 0000000000..934a4637b5 --- /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 0000000000..0597b1d54f --- /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 0000000000..565f5c02ec --- /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 0000000000..fda64e4677 --- /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 0000000000..8c6cb44881 --- /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 0000000000..d6e651b9e4 --- /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 0000000000..7303e1a44b --- /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 0000000000..52bbeac76e --- /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 5369c11956..705a1501eb 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 6123f73bcf..4d95a7a2e0 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 016580a17f..172f661211 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 10d3d07d85..38bed9b9eb 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 9215487f5e..8ee6fbf4c1 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 55a4e1d81d..e81c4094d6 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 8aa6bbecee..5c865e3af0 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 0000000000..6448b919b9 --- /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 0000000000..2775ce1d13 --- /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 292de0a4b0..0d2e15abc5 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 b10e15917f..853c01a4f5 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 12c5de2615..414fa21835 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 0000000000..d854525c29 --- /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 f41ba3c1e7..3f730960d9 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 0000000000..192ade51d2 --- /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 5b44a30f5a..101f965ab1 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 8eec17bd07..6202fb99e3 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 9f6f1b8292..f683e44e44 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 4816f1cc5d..a7fb1d0aa0 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 cb0dbf4b19..2fec9cbf39 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 a07795a719..b512fa852a 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 323ed575a4..3cd3bc662f 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 0000000000..c6a4e421ee --- /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 0000000000..a4a48131d4 --- /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 6654fa9b21..8aecc2bd9a 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 0000000000..df1ffab630 --- /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 0000000000..4458d16496 --- /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 0000000000..c257825fa1 --- /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 0000000000..1822b8517a --- /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 0000000000..923d79029c --- /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 0000000000..f64c18ec52 --- /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 0aea14adbe..a17fd744c1 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 3b26329b0c..f39c53bc9f 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 0000000000..6d44e722ce --- /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 62c1771bd2..58c808f495 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 47ada41eff..0000000000 --- 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 0000000000..4e4fed010e --- /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 0000000000..6feb0fea04 --- /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 06b4c2f5d1..645949e74a 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 2e8c7a3fd4..afdf59ba34 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 80e4dc3a49..3ff83eaba3 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 0000000000..7b8e26e1d4 --- /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 0000000000..bb57e68222 --- /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 0000000000..e0aa6e2c37 --- /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 0000000000..84a2edb9d0 --- /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 0000000000..9bae1d46a7 --- /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 0000000000..f6c6db6b0e --- /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 0000000000..0c66da66d4 --- /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 0000000000..a70f2f7d98 --- /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 0000000000..467216f8f4 --- /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 49231c6ff5..a8cb100a52 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 0000000000..8cb28f0d38 --- /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 0000000000..e5c24cae3a --- /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 0000000000..2fbd2a8853 --- /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 0000000000..bb5c0811ab --- /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 2b2f40cb92..b030fb84e5 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 84475ac03e..6b50b3c813 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 0000000000..6e46a32e72 --- /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 0000000000..567018380d --- /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 e27e09ca92..bc365793ef 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 d57d5825ed..af2362174d 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 0000000000..d3ae3cfb84 --- /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 0000000000..d5b2d20db8 --- /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 0000000000..94f5f798d8 --- /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 a208d89c54..70737bb422 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 0000000000..7df2ad8b01 --- /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 0000000000..0b094b2b42 --- /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 da39dc3a20..6335f4c70b 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 790de2176a..fe1e93ac20 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 a018930377..97784762e7 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 7a16cf5221..9b12ccbd0b 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 1239dd41dc..6528f0ac31 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 7b346585e5..7e706cd3f8 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 1c4ae17fc0..7f8c1c8d1b 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 ac81bfeef3..1ee58482b3 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 8210919515..0000000000 --- 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 0000000000..d7201ae7f9 --- /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 0000000000..27bfb56436 --- /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 7c35038ef6..d61a777ab3 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 0000000000..ff86151196 --- /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 0000000000..cd4dfb4895 --- /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 0000000000..329314d843 --- /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 3936ba330a..dcfd011ca8 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 0000000000..4eca6fd064 --- /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 0000000000..04f7d4a9ab --- /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 0000000000..38571c2300 --- /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 0000000000..9bd916cd74 --- /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 0000000000..416b3f0a1c --- /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 0000000000..6cc31f9546 --- /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 0000000000..ca4703c05e --- /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 0000000000..4548ea5f5d --- /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 0000000000..b854ab7a60 --- /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 0000000000..cd4d20cd05 --- /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 0000000000..93b87b03e8 --- /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 0000000000..4be128be8c --- /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 f456788b07..db1101e2bb 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 6bdba9dd17..8d53e3c141 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 242a7f1e61..0724181706 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 0000000000..7f18c9ed43 --- /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 0000000000..4e326bb9d9 --- /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 8cccb5df7b..0902bbdb61 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 f88d0122d8..0f0dcb647b 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 e2c39264ec..7b09bc39bd 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 0000000000..efadeb21d1 --- /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 0000000000..5459cfdf5f --- /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 0000000000..85904bb3b7 --- /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 0000000000..a8f1afd40f --- /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 41240d58c6..7346ebd342 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 0000000000..2da2c86daf --- /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 0000000000..658a694c64 --- /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 0000000000..1d8966ef1f --- /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 0000000000..48c2e48012 --- /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 0000000000..d4365b82f7 --- /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 f8c891f87d..9f1725f881 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 085856d6f7..8cfb1ce11d 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 02888e38c4..8e6341c092 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 0000000000..022055ca29 --- /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 7e9752278e..aaa870e136 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 0000000000..0ccda6f689 --- /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 19b2962386..54d772977a 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. From 89664a281345321b62d9231cd98f9330d459ac45 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Thu, 13 Feb 2025 22:49:05 +0000 Subject: [PATCH 09/76] New Crowdin translations by GitHub Action --- amethyst/src/main/res/values-cy-rGB/strings.xml | 2 +- amethyst/src/main/res/values-da-rDK/strings.xml | 2 +- amethyst/src/main/res/values-en-rGB/strings.xml | 2 +- amethyst/src/main/res/values-gu-rIN/strings.xml | 2 +- amethyst/src/main/res/values-hi-rIN/strings.xml | 4 ++++ amethyst/src/main/res/values-hr-rHR/strings.xml | 2 +- amethyst/src/main/res/values-hu/strings.xml | 4 ++++ amethyst/src/main/res/values-iw-rIL/strings.xml | 2 +- amethyst/src/main/res/values-kk-rKZ/strings.xml | 2 +- amethyst/src/main/res/values-ko-rKR/strings.xml | 2 +- amethyst/src/main/res/values-ks-rIN/strings.xml | 2 +- amethyst/src/main/res/values-lt-rLT/strings.xml | 2 +- amethyst/src/main/res/values-ne-rNP/strings.xml | 2 +- amethyst/src/main/res/values-nl/strings.xml | 4 ++++ amethyst/src/main/res/values-pcm-rNG/strings.xml | 2 +- amethyst/src/main/res/values-pl-rPL/strings.xml | 10 +++++++--- amethyst/src/main/res/values-pt-rPT/strings.xml | 2 +- amethyst/src/main/res/values-ru-rUA/strings.xml | 2 +- amethyst/src/main/res/values-sa-rIN/strings.xml | 2 +- amethyst/src/main/res/values-sr-rSP/strings.xml | 2 +- amethyst/src/main/res/values-sw-rTZ/strings.xml | 2 +- amethyst/src/main/res/values-tr/strings.xml | 2 +- amethyst/src/main/res/values-uz-rUZ/strings.xml | 2 +- amethyst/src/main/res/values-vi-rVN/strings.xml | 2 +- amethyst/src/main/res/values-zh-rSG/strings.xml | 2 +- 25 files changed, 40 insertions(+), 24 deletions(-) diff --git a/amethyst/src/main/res/values-cy-rGB/strings.xml b/amethyst/src/main/res/values-cy-rGB/strings.xml index 3ea04e700d..66a570486b 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 3ea04e700d..66a570486b 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 48dcf6594e..2e780a1344 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 3ea04e700d..66a570486b 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-hi-rIN/strings.xml b/amethyst/src/main/res/values-hi-rIN/strings.xml index 19fb944a7f..fa8659afec 100644 --- a/amethyst/src/main/res/values-hi-rIN/strings.xml +++ b/amethyst/src/main/res/values-hi-rIN/strings.xml @@ -539,6 +539,8 @@ सम्पूर्ण सरलीकृत वेगवान + प्राचीन + आधुनिक यन्त्रव्यवस्था प्रकाशवान अन्धकारमय @@ -553,6 +555,8 @@ मार्गदर्शनपट्टियों को छिपाएँ पृष्ठघुमाव करने पर प्रयोगमाध्यम शैली पत्र प्रकाशन शैली का चयन करें + परिचय चित्रालय शैली + चित्रालय शैली चुनें चित्र प्राप्त करें कचरालेख प्रेषक मौन किया गया। अमौन करने के लिए टाँकें diff --git a/amethyst/src/main/res/values-hr-rHR/strings.xml b/amethyst/src/main/res/values-hr-rHR/strings.xml index 3ea04e700d..66a570486b 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-hu/strings.xml b/amethyst/src/main/res/values-hu/strings.xml index 0f73f77874..e51be13e02 100644 --- a/amethyst/src/main/res/values-hu/strings.xml +++ b/amethyst/src/main/res/values-hu/strings.xml @@ -539,6 +539,8 @@ Teljes Egyszerűsített Teljesítmény + Klasszikus + Modern Rendszer Világos Sötét @@ -553,6 +555,8 @@ Navigációs sáv elrejtése görgetéskor Felhasználói felület módja Bejegyzés stílusának kiválasztása + Profilgaléria stílusa + Válassza ki a galéria stílusát Kép betöltése Spamelők Némítva. Koppintson a némitás megszüntetéséhez diff --git a/amethyst/src/main/res/values-iw-rIL/strings.xml b/amethyst/src/main/res/values-iw-rIL/strings.xml index 3ea04e700d..66a570486b 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 3ea04e700d..66a570486b 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 3ea04e700d..66a570486b 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 3ea04e700d..66a570486b 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 3ea04e700d..66a570486b 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 3ea04e700d..66a570486b 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-nl/strings.xml b/amethyst/src/main/res/values-nl/strings.xml index cb6b179a24..daa6f5f5d8 100644 --- a/amethyst/src/main/res/values-nl/strings.xml +++ b/amethyst/src/main/res/values-nl/strings.xml @@ -539,6 +539,8 @@ Voltooid Vereenvoudigd Prestaties + Klassiek + Modern Systeem Licht Donker @@ -553,6 +555,8 @@ Navigatie verbergen bij scrollen UI modus Kies de stijl van het bericht + Profielgalerij stijl + Kies de galerijstijl Afbeelding laden Spammers Geen geluid. Klik om voor geluid diff --git a/amethyst/src/main/res/values-pcm-rNG/strings.xml b/amethyst/src/main/res/values-pcm-rNG/strings.xml index 3ea04e700d..66a570486b 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-pl-rPL/strings.xml b/amethyst/src/main/res/values-pl-rPL/strings.xml index 7bbddd0b48..892e11d8ef 100644 --- a/amethyst/src/main/res/values-pl-rPL/strings.xml +++ b/amethyst/src/main/res/values-pl-rPL/strings.xml @@ -7,7 +7,7 @@ Zeskanuj QR kod Pokaż mimo wszystko Ten post został ukryty, ponieważ dotyczy ukrytych użytkowników lub słów - Post został wyciszony lub zgłoszony przez + Wpis został ukryty lub zgłoszony przez Wydarzenie jest wczytywane lub nie można go znaleźć na liście transmiterów 👀 Zdjęcie kanału @@ -80,7 +80,7 @@ Wiadomość dla odbiorcy Dziękuję bardzo! Kwota w Satsach - Wyślij Satsy + Wyślij "Błąd analizowania podglądu dla %1$s : %2$s" "Podgląd obrazu karty dla %1$s" Nowy kanał @@ -539,6 +539,8 @@ Kompletny Uproszczony Dynamiczny + Klasyczny + Nowoczesny Automatyczny Jasny Ciemny @@ -553,9 +555,11 @@ Ukrywa pasek przewijania Tryb interfejsu Wybierz styl wpisu + Styl galerii + Wybierz styl galerii Załaduj obraz Spamerzy - Wyciszone. Kliknij, aby wyłączyć wyciszenie + Wyciszone. Kliknij, aby włączyć dźwięk Dźwięk włączony. Kliknij, aby wyciszyć Wyszukiwanie lokalne i zdalne Adres Nostr został zweryfikowany diff --git a/amethyst/src/main/res/values-pt-rPT/strings.xml b/amethyst/src/main/res/values-pt-rPT/strings.xml index 3ea04e700d..66a570486b 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 3ea04e700d..66a570486b 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 3ea04e700d..66a570486b 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 4e434e5ce6..25197aa216 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 3ea04e700d..66a570486b 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 0cd4766e16..0c9d8ae64a 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 9a05f357d9..4fec38d164 100644 --- a/amethyst/src/main/res/values-uz-rUZ/strings.xml +++ b/amethyst/src/main/res/values-uz-rUZ/strings.xml @@ -1,5 +1,5 @@ - + Kamerani QR kodga yo\'naltiring QR kodni ko‘rsatish Profil rasmini diff --git a/amethyst/src/main/res/values-vi-rVN/strings.xml b/amethyst/src/main/res/values-vi-rVN/strings.xml index 2392d4a794..d374dcabd9 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 3ea04e700d..66a570486b 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 @@ - + From 0610416f612a85989d8cc39f2ae83e27177d6ba4 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 13 Feb 2025 17:59:23 -0500 Subject: [PATCH 10/76] Updates AGP to 8.8.1 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 448648f3a9..2072d295bc 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] accompanistAdaptive = "0.37.0" activityCompose = "1.9.3" -agp = "8.8.0" +agp = "8.8.1" android-compileSdk = "35" android-minSdk = "26" android-targetSdk = "35" From cb121835f59b8d58d98786e3d86fa4808a5c5e72 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 13 Feb 2025 19:01:30 -0500 Subject: [PATCH 11/76] Stops using vico's forEach custom function (it's going to be deleted in the next version) --- .../ui/screen/loggedIn/chatrooms/ChatroomHeaderCompose.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 7f89146749..1e6edaaa77 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 @@ -56,7 +56,6 @@ import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.distinctUntilChanged import androidx.lifecycle.map -import com.patrykandpatrick.vico.core.extension.forEachIndexedExtended import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.model.FeatureSetType @@ -86,6 +85,7 @@ 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 +import kotlin.math.min @Composable fun ChatroomHeaderCompose( @@ -372,12 +372,12 @@ fun DisplayUserSetAsSubject( } } else { Row { - userList.take(4).forEachIndexedExtended { index, isFirst, isLast, value -> + userList.take(4).forEachIndexed { index, value -> LoadUser(baseUserHex = value, accountViewModel) { it?.let { ShortUsernameDisplay(baseUser = it, fontWeight = fontWeight, accountViewModel = accountViewModel) } } - if (!isLast) { + if (min(userList.size, 4) - 1 != index) { Text( text = ", ", fontWeight = fontWeight, From dcb0e31cd6d2d46bf4c891f96a9d60630b5df6ec Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 13 Feb 2025 19:02:15 -0500 Subject: [PATCH 12/76] Updating activity compose, coil, combosebom, firebasebom, fragmentktx, kotlinserialization, mockk and navigation compose --- gradle/libs.versions.toml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2072d295bc..5d171da548 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] accompanistAdaptive = "0.37.0" -activityCompose = "1.9.3" +activityCompose = "1.10.0" agp = "8.8.1" android-compileSdk = "35" android-minSdk = "26" @@ -12,12 +12,12 @@ audiowaveform = "1.1.1" benchmark = "1.3.3" benchmarkJunit4 = "1.3.3" biometricKtx = "1.2.0-alpha05" -coil = "3.0.4" -composeBom = "2024.12.01" +coil = "3.1.0" +composeBom = "2025.02.00" coreKtx = "1.15.0" espressoCore = "3.6.1" -firebaseBom = "33.7.0" -fragmentKtx = "1.8.5" +firebaseBom = "33.9.0" +fragmentKtx = "1.8.6" gms = "4.4.2" jacksonModuleKotlin = "2.18.2" jna = "5.16.0" @@ -25,7 +25,7 @@ jtorctl = "0.4.5.7" junit = "4.13.2" kotlin = "2.1.0" kotlinxCollectionsImmutable = "0.3.8" -kotlinxSerialization = "1.7.3" +kotlinxSerialization = "1.8.0" kotlinxSerializationPlugin = "2.0.0" languageId = "17.0.6" lazysodiumAndroid = "5.1.0" @@ -33,9 +33,9 @@ lifecycleRuntimeKtx = "2.8.7" lightcompressor = "1.3.2" markdown = "077a2cde64" media3 = "1.5.1" -mockk = "1.13.14" +mockk = "1.13.16" kotlinx-coroutines-test = "1.10.1" -navigationCompose = "2.8.5" +navigationCompose = "2.8.7" okhttp = "5.0.0-alpha.14" runner = "1.6.2" rfc3986 = "0.1.2" From 0f6c3b9a3acbfef6528becdf87783a68de130413 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 14 Feb 2025 11:05:00 -0500 Subject: [PATCH 13/76] Moves to vico chart 2.0 --- ...kt => ThreadDualAxisChartAssemblerTest.kt} | 2 +- .../amethyst/ui/note/ChannelCardCompose.kt | 3 +- .../amethyst/ui/note/ZapFormatter.kt | 7 + .../ui/note/ZapFormatterNoDecimals.kt | 64 ++++++++ .../amethyst/ui/note/ZapNoteCompose.kt | 3 +- .../ui/screen/loggedIn/AccountViewModel.kt | 22 +-- .../notifications/NotificationSummaryState.kt | 130 ++++++--------- .../notifications/NotificationSummaryView.kt | 155 +----------------- .../chart/AmountValueFormatter.kt | 43 +++++ .../chart/CountAxisValueFormatter.kt | 47 ++++++ .../chart/LastWeekLabelFormatter.kt | 51 ++++++ .../loggedIn/notifications/chart/ShowChart.kt | 101 ++++++++++++ .../screen/loggedIn/profile/ProfileScreen.kt | 4 +- .../vitorpamplona/amethyst/ui/theme/Theme.kt | 48 ++++-- gradle/libs.versions.toml | 2 +- 15 files changed, 419 insertions(+), 263 deletions(-) rename amethyst/src/androidTest/java/com/vitorpamplona/amethyst/{ThreadAssemblerTest.kt => ThreadDualAxisChartAssemblerTest.kt} (99%) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapFormatterNoDecimals.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/chart/AmountValueFormatter.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/chart/CountAxisValueFormatter.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/chart/LastWeekLabelFormatter.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/chart/ShowChart.kt diff --git a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ThreadAssemblerTest.kt b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ThreadDualAxisChartAssemblerTest.kt similarity index 99% rename from amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ThreadAssemblerTest.kt rename to amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ThreadDualAxisChartAssemblerTest.kt index c45397357c..215e089cdb 100644 --- a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ThreadAssemblerTest.kt +++ b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ThreadDualAxisChartAssemblerTest.kt @@ -42,7 +42,7 @@ import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) -class ThreadAssemblerTest { +class ThreadDualAxisChartAssemblerTest { val db = """ [ 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 2ffe22d511..ae66ef69fa 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 @@ -86,7 +86,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.ScheduledFlag import com.vitorpamplona.amethyst.ui.screen.loggedIn.dvms.observeAppDefinition import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.CheckIfVideoIsOnline import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists -import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.showAmountAxis import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer import com.vitorpamplona.amethyst.ui.theme.HalfPadding @@ -386,7 +385,7 @@ fun InnerRenderClassifiedsThumb( card.price?.let { val priceTag = remember(card) { - val newAmount = it.amount.toBigDecimalOrNull()?.let { showAmountAxis(it) } ?: it.amount + val newAmount = it.amount.toBigDecimalOrNull()?.let { showAmountInteger(it) } ?: it.amount if (it.frequency != null && it.currency != null) { "$newAmount ${it.currency}/${it.frequency}" diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapFormatter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapFormatter.kt index 0ad29ac7f1..e5024ad675 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapFormatter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapFormatter.kt @@ -74,3 +74,10 @@ fun showAmount(amount: BigDecimal?): String { else -> dfN.get().format(amount) } } + +fun showAmountWithZero(amount: BigDecimal?): String { + if (amount == null) return "0" + if (amount.abs() < BigDecimal(0.01)) return "0" + + return showAmount(amount) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapFormatterNoDecimals.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapFormatterNoDecimals.kt new file mode 100644 index 0000000000..d36f532420 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapFormatterNoDecimals.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.amethyst.ui.note + +import java.math.BigDecimal +import java.math.RoundingMode +import java.text.DecimalFormat + +private val dfG = + object : ThreadLocal() { + override fun initialValue() = DecimalFormat("#G") + } + +private val dfM = + object : ThreadLocal() { + override fun initialValue() = DecimalFormat("#M") + } + +private val dfK = + object : ThreadLocal() { + override fun initialValue() = DecimalFormat("#k") + } + +private val dfN = + object : ThreadLocal() { + override fun initialValue() = DecimalFormat("#") + } + +fun showAmountInteger(amount: BigDecimal?): String { + if (amount == null) return "" + if (amount.abs() < BigDecimal(0.01)) return "" + + return when { + amount >= OneGiga -> dfG.get().format(amount.div(OneGiga).setScale(0, RoundingMode.HALF_UP)) + amount >= OneMega -> dfM.get().format(amount.div(OneMega).setScale(0, RoundingMode.HALF_UP)) + amount >= TenKilo -> dfK.get().format(amount.div(OneKilo).setScale(0, RoundingMode.HALF_UP)) + else -> dfN.get().format(amount) + } +} + +fun showAmountIntegerWithZero(amount: BigDecimal?): String { + if (amount == null) return "0" + if (amount.abs() < BigDecimal(0.01)) return "0" + + return showAmountInteger(amount) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt index b58a236922..385502963e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt @@ -47,7 +47,6 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.showAmountAxis import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.FollowButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.ShowUserButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.UnfollowButton @@ -144,7 +143,7 @@ private fun ZapAmount(zapEventNote: Note) { LaunchedEffect(key1 = noteState) { launch(Dispatchers.IO) { - val newZapAmount = showAmountAxis((noteState?.note?.event as? LnZapEvent)?.amount) + val newZapAmount = showAmountInteger((noteState?.note?.event as? LnZapEvent)?.amount) if (zapAmount != newZapAmount) { zapAmount = newZapAmount } 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 7d246191ca..b9cbc1ef15 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 @@ -64,11 +64,11 @@ import com.vitorpamplona.amethyst.ui.navigation.Route import com.vitorpamplona.amethyst.ui.note.ZapAmountCommentNotification import com.vitorpamplona.amethyst.ui.note.ZapraiserStatus import com.vitorpamplona.amethyst.ui.note.showAmount +import com.vitorpamplona.amethyst.ui.note.showAmountInteger import com.vitorpamplona.amethyst.ui.screen.SettingsState import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.CardFeedState import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.CombinedZap -import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.showAmountAxis import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.tor.TorSettings import com.vitorpamplona.ammolite.relays.BundledInsert @@ -536,7 +536,7 @@ class AccountViewModel( it.request.event ?.content ?.ifBlank { null }, - showAmountAxis((it.response.event as? LnZapEvent)?.amount), + showAmountInteger((it.response.event as? LnZapEvent)?.amount), ) }.toMutableMap() @@ -569,7 +569,7 @@ class AccountViewModel( ZapAmountCommentNotification( LocalCache.getUserIfExists(cachedPrivateRequest.pubKey) ?: it.request.author, cachedPrivateRequest.content.ifBlank { null }, - showAmountAxis((it.response.event as? LnZapEvent)?.amount), + showAmountInteger((it.response.event as? LnZapEvent)?.amount), ) } else { ZapAmountCommentNotification( @@ -577,7 +577,7 @@ class AccountViewModel( it.request.event ?.content ?.ifBlank { null }, - showAmountAxis((it.response.event as? LnZapEvent)?.amount), + showAmountInteger((it.response.event as? LnZapEvent)?.amount), ) } } else { @@ -586,7 +586,7 @@ class AccountViewModel( it.request.event ?.content ?.ifBlank { null }, - showAmountAxis((it.response.event as? LnZapEvent)?.amount), + showAmountInteger((it.response.event as? LnZapEvent)?.amount), ) } }.toImmutableList() @@ -603,7 +603,7 @@ class AccountViewModel( ZapAmountCommentNotification( LocalCache.getUserIfExists(cachedPrivateRequest.pubKey) ?: it.first.author, cachedPrivateRequest.content.ifBlank { null }, - showAmountAxis((it.second?.event as? LnZapEvent)?.amount), + showAmountInteger((it.second?.event as? LnZapEvent)?.amount), ) } else { ZapAmountCommentNotification( @@ -611,7 +611,7 @@ class AccountViewModel( it.first.event ?.content ?.ifBlank { null }, - showAmountAxis((it.second?.event as? LnZapEvent)?.amount), + showAmountInteger((it.second?.event as? LnZapEvent)?.amount), ) } } else { @@ -620,7 +620,7 @@ class AccountViewModel( it.first.event ?.content ?.ifBlank { null }, - showAmountAxis((it.second?.event as? LnZapEvent)?.amount), + showAmountInteger((it.second?.event as? LnZapEvent)?.amount), ) } }.toImmutableList() @@ -642,7 +642,7 @@ class AccountViewModel( it.first.event ?.content ?.ifBlank { null }, - showAmountAxis((it.second?.event as? LnZapEvent)?.amount), + showAmountInteger((it.second?.event as? LnZapEvent)?.amount), ) }.toMutableMap() @@ -687,7 +687,7 @@ class AccountViewModel( ZapAmountCommentNotification( newAuthor, decryptedContent.content.ifBlank { null }, - showAmountAxis(amount), + showAmountInteger(amount), ), ) } @@ -698,7 +698,7 @@ class AccountViewModel( ZapAmountCommentNotification( zapRequest.author, zapRequest.event?.content?.ifBlank { null }, - showAmountAxis(amount), + showAmountInteger(amount), ), ) } 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 b6ea64b987..aef6b48e77 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 @@ -22,16 +22,16 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications import android.util.Log import androidx.compose.runtime.Stable -import com.patrykandpatrick.vico.core.chart.composed.ComposedChartEntryModel -import com.patrykandpatrick.vico.core.entry.ChartEntryModel -import com.patrykandpatrick.vico.core.entry.ChartEntryModelProducer -import com.patrykandpatrick.vico.core.entry.composed.plus -import com.patrykandpatrick.vico.core.entry.entryOf +import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModel +import com.patrykandpatrick.vico.core.cartesian.data.LineCartesianLayerModel +import com.patrykandpatrick.vico.core.common.data.ExtraStore +import com.patrykandpatrick.vico.core.common.data.MutableExtraStore import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.checkNotInMainThread +import com.vitorpamplona.amethyst.ui.note.showAmountInteger import com.vitorpamplona.amethyst.ui.note.showCount import com.vitorpamplona.ammolite.relays.BundledInsert import com.vitorpamplona.quartz.nip01Core.HexKey @@ -52,37 +52,30 @@ import java.time.LocalDateTime import java.time.ZoneId import java.time.format.DateTimeFormatter +val ShowDecimals = ExtraStore.Key() +val BottomAxisLabelKey = ExtraStore.Key>() + @Stable class NotificationSummaryState( val account: Account, ) { val user: User = account.userProfile() - private var _reactions = MutableStateFlow>(emptyMap()) - private var _boosts = MutableStateFlow>(emptyMap()) - private var _zaps = MutableStateFlow>(emptyMap()) - private var _replies = MutableStateFlow>(emptyMap()) - - private var _chartModel = MutableStateFlow?>(null) - private var _axisLabels = MutableStateFlow>(emptyList()) - - val reactions = _reactions.asStateFlow() - val boosts = _boosts.asStateFlow() - val zaps = _zaps.asStateFlow() - val replies = _replies.asStateFlow() + private var reactions = MutableStateFlow>(emptyMap()) + private var boosts = MutableStateFlow>(emptyMap()) + private var zaps = MutableStateFlow>(emptyMap()) + private var replies = MutableStateFlow>(emptyMap()) + private var _chartModel = MutableStateFlow(null) val chartModel = _chartModel.asStateFlow() - val axisLabels = _axisLabels.asStateFlow() private var takenIntoAccount = setOf() private val sdf = DateTimeFormatter.ofPattern("yyyy-MM-dd") // SimpleDateFormat() - val todaysReplyCount = _replies.map { showCount(it[today()]) }.distinctUntilChanged() - val todaysBoostCount = _boosts.map { showCount(it[today()]) }.distinctUntilChanged() - val todaysReactionCount = _reactions.map { showCount(it[today()]) }.distinctUntilChanged() - val todaysZapAmount = _zaps.map { showAmountAxis(it[today()]) }.distinctUntilChanged() - - var shouldShowDecimalsInAxis = false + val todaysReplyCount = replies.map { showCount(it[today()]) }.distinctUntilChanged() + val todaysBoostCount = boosts.map { showCount(it[today()]) }.distinctUntilChanged() + val todaysReactionCount = reactions.map { showCount(it[today()]) }.distinctUntilChanged() + val todaysZapAmount = zaps.map { showAmountInteger(it[today()]) }.distinctUntilChanged() fun formatDate(createAt: Long): String = sdf.format( @@ -146,10 +139,10 @@ class NotificationSummaryState( } this.takenIntoAccount = takenIntoAccount - this._reactions.emit(reactions) - this._replies.emit(replies) - this._zaps.emit(zaps) - this._boosts.emit(boosts) + this.reactions.emit(reactions) + this.replies.emit(replies) + this.zaps.emit(zaps) + this.boosts.emit(boosts) refreshChartModel() } @@ -159,10 +152,10 @@ class NotificationSummaryState( val currentUser = user.pubkeyHex - val reactions = this._reactions.value.toMutableMap() - val boosts = this._boosts.value.toMutableMap() - val zaps = this._zaps.value.toMutableMap() - val replies = this._replies.value.toMutableMap() + val reactions = this.reactions.value.toMutableMap() + val boosts = this.boosts.value.toMutableMap() + val zaps = this.zaps.value.toMutableMap() + val replies = this.replies.value.toMutableMap() val takenIntoAccount = this.takenIntoAccount.toMutableSet() var hasNewElements = false @@ -217,10 +210,10 @@ class NotificationSummaryState( if (hasNewElements) { this.takenIntoAccount = takenIntoAccount - this._reactions.emit(reactions) - this._replies.emit(replies) - this._zaps.emit(zaps) - this._boosts.emit(boosts) + this.reactions.emit(reactions) + this.replies.emit(replies) + this.zaps.emit(zaps) + this.boosts.emit(boosts) refreshChartModel() } @@ -229,59 +222,42 @@ class NotificationSummaryState( private suspend fun refreshChartModel() { checkNotInMainThread() - val day = 24 * 60 * 60L val now = LocalDateTime.now() - val displayAxisFormatter = DateTimeFormatter.ofPattern("EEE") - val dataAxisLabels = listOf(6, 5, 4, 3, 2, 1, 0).map { sdf.format(now.minusSeconds(day * it)) } + val dataAxisLabelIndexes = listOf(-6, -5, -4, -3, -2, -1, 0) + val dataAxisLabels = dataAxisLabelIndexes.map { sdf.format(now.plusDays(it.toLong())) } - val listOfCountCurves = - listOf( - dataAxisLabels.mapIndexed { index, dateStr -> - entryOf(index, _replies.value[dateStr]?.toFloat() ?: 0f) - }, - dataAxisLabels.mapIndexed { index, dateStr -> - entryOf(index, _boosts.value[dateStr]?.toFloat() ?: 0f) - }, - dataAxisLabels.mapIndexed { index, dateStr -> - entryOf(index, _reactions.value[dateStr]?.toFloat() ?: 0f) - }, - ) - - val listOfValueCurves = - listOf( - dataAxisLabels.mapIndexed { index, dateStr -> - entryOf(index, _zaps.value[dateStr]?.toFloat() ?: 0f) - }, - ) - - val chartEntryModelProducer1 = ChartEntryModelProducer(listOfCountCurves).getModel() - val chartEntryModelProducer2 = ChartEntryModelProducer(listOfValueCurves).getModel() - - chartEntryModelProducer1?.let { chart1 -> - chartEntryModelProducer2?.let { chart2 -> - this.shouldShowDecimalsInAxis = shouldShowDecimals(chart2.minY, chart2.maxY) - - this._axisLabels.emit( - listOf(6, 5, 4, 3, 2, 1, 0).map { - displayAxisFormatter.format(now.minusSeconds(day * it)) - }, - ) - this._chartModel.emit(chart1.plus(chart2)) + val chart1 = + LineCartesianLayerModel.build { + series(dataAxisLabelIndexes, dataAxisLabels.map { replies.value[it]?.toFloat() ?: 0f }) + series(dataAxisLabelIndexes, dataAxisLabels.map { boosts.value[it]?.toFloat() ?: 0f }) + series(dataAxisLabelIndexes, dataAxisLabels.map { reactions.value[it]?.toFloat() ?: 0f }) } - } + + val chart2 = + LineCartesianLayerModel.build { + series(dataAxisLabelIndexes, dataAxisLabels.map { zaps.value[it]?.toFloat() ?: 0f }) + } + + val model = CartesianChartModel(chart1, chart2) + + val mutableStore = MutableExtraStore() + + mutableStore[ShowDecimals] = shouldShowDecimals(chart2.minY, chart2.maxY) + + this._chartModel.emit(model.copy(mutableStore)) } // determine if the min max are so close that they render to the same number. fun shouldShowDecimals( - min: Float, - max: Float, + min: Double, + max: Double, ): Boolean { val step = (max - min) / 8 - var previous = showAmountAxis(min.toBigDecimal()) + var previous = showAmountInteger(min.toBigDecimal()) for (i in 1..7) { - val current = showAmountAxis((min + (i * step)).toBigDecimal()) + val current = showAmountInteger((min + (i * step)).toBigDecimal()) if (previous == current) { return true } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationSummaryView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationSummaryView.kt index 7e16002b2c..c8b4717d0d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationSummaryView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationSummaryView.kt @@ -30,47 +30,17 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.Stable -import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.patrykandpatrick.vico.compose.axis.axisLabelComponent -import com.patrykandpatrick.vico.compose.axis.horizontal.rememberBottomAxis -import com.patrykandpatrick.vico.compose.axis.vertical.rememberEndAxis -import com.patrykandpatrick.vico.compose.axis.vertical.rememberStartAxis -import com.patrykandpatrick.vico.compose.chart.Chart -import com.patrykandpatrick.vico.compose.chart.line.lineChart -import com.patrykandpatrick.vico.compose.component.shape.shader.fromBrush -import com.patrykandpatrick.vico.compose.style.ProvideChartStyle -import com.patrykandpatrick.vico.core.DefaultAlpha -import com.patrykandpatrick.vico.core.axis.AxisPosition -import com.patrykandpatrick.vico.core.axis.formatter.AxisValueFormatter -import com.patrykandpatrick.vico.core.chart.composed.plus -import com.patrykandpatrick.vico.core.chart.line.LineChart -import com.patrykandpatrick.vico.core.chart.values.ChartValues -import com.patrykandpatrick.vico.core.component.shape.shader.DynamicShaders -import com.vitorpamplona.amethyst.ui.note.OneGiga -import com.vitorpamplona.amethyst.ui.note.OneKilo -import com.vitorpamplona.amethyst.ui.note.OneMega -import com.vitorpamplona.amethyst.ui.note.TenKilo +import com.patrykandpatrick.vico.compose.common.ProvideVicoTheme import com.vitorpamplona.amethyst.ui.note.UserReactionsRow -import com.vitorpamplona.amethyst.ui.note.showAmount -import com.vitorpamplona.amethyst.ui.note.showCount -import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange -import com.vitorpamplona.amethyst.ui.theme.RoyalBlue +import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.chart.ShowChart import com.vitorpamplona.amethyst.ui.theme.chartStyle -import java.math.BigDecimal -import java.math.RoundingMode -import java.text.DecimalFormat -import kotlin.math.roundToInt @Composable fun SummaryBar(state: NotificationSummaryState) { @@ -83,137 +53,24 @@ fun SummaryBar(state: NotificationSummaryState) { enter = slideInVertically() + expandVertically(), exit = slideOutVertically() + shrinkVertically(), ) { - val lineChartCount = - lineChart( - lines = - listOf(RoyalBlue, Color.Green, Color.Red).map { lineChartColor -> - LineChart.LineSpec( - lineColor = lineChartColor.toArgb(), - lineBackgroundShader = - DynamicShaders.fromBrush( - Brush.verticalGradient( - listOf( - lineChartColor.copy(DefaultAlpha.LINE_BACKGROUND_SHADER_START), - lineChartColor.copy(DefaultAlpha.LINE_BACKGROUND_SHADER_END), - ), - ), - ), - ) - }, - targetVerticalAxisPosition = AxisPosition.Vertical.Start, - ) - - val lineChartZaps = - lineChart( - lines = - listOf(BitcoinOrange).map { lineChartColor -> - LineChart.LineSpec( - lineColor = lineChartColor.toArgb(), - lineBackgroundShader = - DynamicShaders.fromBrush( - Brush.verticalGradient( - listOf( - lineChartColor.copy(DefaultAlpha.LINE_BACKGROUND_SHADER_START), - lineChartColor.copy(DefaultAlpha.LINE_BACKGROUND_SHADER_END), - ), - ), - ), - ) - }, - targetVerticalAxisPosition = AxisPosition.Vertical.End, - ) - Row( modifier = Modifier .padding(vertical = 0.dp, horizontal = 20.dp) .clickable(onClick = { showChart = !showChart }), ) { - ProvideChartStyle( - chartStyle = MaterialTheme.colorScheme.chartStyle, - ) { - ObserveAndShowChart(state, lineChartCount, lineChartZaps) + ProvideVicoTheme(MaterialTheme.colorScheme.chartStyle) { + ObserveAndShowChart(state) } } } } @Composable -private fun ObserveAndShowChart( - state: NotificationSummaryState, - lineChartCount: LineChart, - lineChartZaps: LineChart, -) { - val axisModel = state.axisLabels.collectAsStateWithLifecycle() +private fun ObserveAndShowChart(state: NotificationSummaryState) { val chartModel by state.chartModel.collectAsStateWithLifecycle() chartModel?.let { - Chart( - chart = remember(lineChartCount, lineChartZaps) { lineChartCount.plus(lineChartZaps) }, - model = it, - startAxis = - rememberStartAxis( - valueFormatter = CountAxisValueFormatter(), - ), - endAxis = - rememberEndAxis( - label = axisLabelComponent(color = BitcoinOrange), - valueFormatter = AmountAxisValueFormatter(state.shouldShowDecimalsInAxis), - ), - bottomAxis = - rememberBottomAxis( - valueFormatter = LabelValueFormatter(axisModel), - ), - ) - } -} - -@Stable -class LabelValueFormatter( - val axisLabels: State>, -) : AxisValueFormatter { - override fun formatValue( - value: Float, - chartValues: ChartValues, - ): String = axisLabels.value[value.roundToInt()] -} - -@Stable -class CountAxisValueFormatter : AxisValueFormatter { - override fun formatValue( - value: Float, - chartValues: ChartValues, - ): String = showCount(value.roundToInt()) -} - -@Stable -class AmountAxisValueFormatter( - val showDecimals: Boolean, -) : AxisValueFormatter { - override fun formatValue( - value: Float, - chartValues: ChartValues, - ): String = - if (showDecimals) { - showAmount(value.toBigDecimal()) - } else { - showAmountAxis(value.toBigDecimal()) - } -} - -var dfG: DecimalFormat = DecimalFormat("#G") -var dfM: DecimalFormat = DecimalFormat("#M") -var dfK: DecimalFormat = DecimalFormat("#k") -var dfN: DecimalFormat = DecimalFormat("#") - -fun showAmountAxis(amount: BigDecimal?): String { - if (amount == null) return "" - if (amount.abs() < BigDecimal(0.01)) return "" - - return when { - amount >= OneGiga -> dfG.format(amount.div(OneGiga).setScale(0, RoundingMode.HALF_UP)) - amount >= OneMega -> dfM.format(amount.div(OneMega).setScale(0, RoundingMode.HALF_UP)) - amount >= TenKilo -> dfK.format(amount.div(OneKilo).setScale(0, RoundingMode.HALF_UP)) - else -> dfN.format(amount) + ShowChart(it) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/chart/AmountValueFormatter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/chart/AmountValueFormatter.kt new file mode 100644 index 0000000000..c1b0a79b0e --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/chart/AmountValueFormatter.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.amethyst.ui.screen.loggedIn.notifications.chart + +import androidx.compose.runtime.Stable +import com.patrykandpatrick.vico.core.cartesian.CartesianMeasuringContext +import com.patrykandpatrick.vico.core.cartesian.axis.Axis +import com.patrykandpatrick.vico.core.cartesian.data.CartesianValueFormatter +import com.vitorpamplona.amethyst.ui.note.showAmountIntegerWithZero +import com.vitorpamplona.amethyst.ui.note.showAmountWithZero +import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.ShowDecimals + +@Stable +class AmountValueFormatter : CartesianValueFormatter { + override fun format( + context: CartesianMeasuringContext, + value: Double, + verticalAxisPosition: Axis.Position.Vertical?, + ): CharSequence = + if (context.model.extraStore[ShowDecimals]) { + showAmountWithZero(value.toBigDecimal()) + } else { + showAmountIntegerWithZero(value.toBigDecimal()) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/chart/CountAxisValueFormatter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/chart/CountAxisValueFormatter.kt new file mode 100644 index 0000000000..1f64239b76 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/chart/CountAxisValueFormatter.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.amethyst.ui.screen.loggedIn.notifications.chart + +import androidx.compose.runtime.Stable +import com.patrykandpatrick.vico.core.cartesian.CartesianMeasuringContext +import com.patrykandpatrick.vico.core.cartesian.axis.Axis +import com.patrykandpatrick.vico.core.cartesian.data.CartesianValueFormatter +import kotlin.math.roundToInt + +@Stable +class CountAxisValueFormatter : CartesianValueFormatter { + private fun showCountChart(count: Int?): String { + if (count == null) return "0" + + return when { + count >= 1000000000 -> "${(count / 1000000000f).roundToInt()}G" + count >= 1000000 -> "${(count / 1000000f).roundToInt()}M" + count >= 10000 -> "${(count / 1000f).roundToInt()}k" + else -> "$count" + } + } + + override fun format( + context: CartesianMeasuringContext, + value: Double, + verticalAxisPosition: Axis.Position.Vertical?, + ): CharSequence = showCountChart(value.roundToInt()) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/chart/LastWeekLabelFormatter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/chart/LastWeekLabelFormatter.kt new file mode 100644 index 0000000000..d9e44e0158 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/chart/LastWeekLabelFormatter.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.amethyst.ui.screen.loggedIn.notifications.chart + +import android.util.LruCache +import androidx.compose.runtime.Stable +import com.patrykandpatrick.vico.core.cartesian.CartesianMeasuringContext +import com.patrykandpatrick.vico.core.cartesian.axis.Axis +import com.patrykandpatrick.vico.core.cartesian.data.CartesianValueFormatter +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import kotlin.math.roundToInt + +@Stable +class LastWeekLabelFormatter : CartesianValueFormatter { + private val displayAxisFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern("EEE") + private val now = LocalDateTime.now() + + private val cache = LruCache(10) + + override fun format( + context: CartesianMeasuringContext, + value: Double, + verticalAxisPosition: Axis.Position.Vertical?, + ): CharSequence { + val key = value.roundToInt() + cache[key]?.let { return it } + + val text = displayAxisFormatter.format(now.plusDays(key.toLong())) + cache.put(key, text) + return text + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/chart/ShowChart.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/chart/ShowChart.kt new file mode 100644 index 0000000000..d3a98858f0 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/chart/ShowChart.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.amethyst.ui.screen.loggedIn.notifications.chart + +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import com.patrykandpatrick.vico.compose.cartesian.CartesianChartHost +import com.patrykandpatrick.vico.compose.cartesian.axis.rememberAxisLabelComponent +import com.patrykandpatrick.vico.compose.cartesian.axis.rememberBottom +import com.patrykandpatrick.vico.compose.cartesian.axis.rememberEnd +import com.patrykandpatrick.vico.compose.cartesian.axis.rememberStart +import com.patrykandpatrick.vico.compose.cartesian.rememberCartesianChart +import com.patrykandpatrick.vico.compose.common.fill +import com.patrykandpatrick.vico.core.cartesian.axis.Axis +import com.patrykandpatrick.vico.core.cartesian.axis.HorizontalAxis +import com.patrykandpatrick.vico.core.cartesian.axis.VerticalAxis +import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModel +import com.patrykandpatrick.vico.core.cartesian.layer.LineCartesianLayer +import com.patrykandpatrick.vico.core.common.shader.ShaderProvider +import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange +import com.vitorpamplona.amethyst.ui.theme.RoyalBlue + +fun makeLine(color: Color): LineCartesianLayer.Line = + LineCartesianLayer.Line( + fill = LineCartesianLayer.LineFill.single(fill(color)), + areaFill = + LineCartesianLayer.AreaFill.single( + fill( + ShaderProvider.verticalGradient( + color.copy(alpha = 0.4f).toArgb(), + Color.Transparent.toArgb(), + ), + ), + ), + pointConnector = LineCartesianLayer.PointConnector.cubic(), + ) + +val chartLayers = + arrayOf( + LineCartesianLayer( + LineCartesianLayer.LineProvider.series( + makeLine(RoyalBlue), + makeLine(Color.Green), + makeLine(Color.Red), + ), + verticalAxisPosition = Axis.Position.Vertical.Start, + ), + LineCartesianLayer( + LineCartesianLayer.LineProvider.series( + makeLine(BitcoinOrange), + ), + verticalAxisPosition = Axis.Position.Vertical.End, + ), + ) + +@Composable +fun ShowChart(model: CartesianChartModel) { + val chart = + rememberCartesianChart( + layers = chartLayers, + startAxis = + VerticalAxis.rememberStart( + valueFormatter = CountAxisValueFormatter(), + itemPlacer = VerticalAxis.ItemPlacer.count({ 7 }), + ), + endAxis = + VerticalAxis.rememberEnd( + label = rememberAxisLabelComponent(color = BitcoinOrange), + valueFormatter = AmountValueFormatter(), + itemPlacer = VerticalAxis.ItemPlacer.count({ 7 }), + ), + bottomAxis = + HorizontalAxis.rememberBottom( + valueFormatter = LastWeekLabelFormatter(), + ), + ) + + CartesianChartHost( + chart = chart, + model = model, + ) +} 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 988eb7096c..2073699c13 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 @@ -145,6 +145,7 @@ import com.vitorpamplona.amethyst.ui.note.LightningAddressIcon import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote import com.vitorpamplona.amethyst.ui.note.externalLinkForUser import com.vitorpamplona.amethyst.ui.note.payViaIntent +import com.vitorpamplona.amethyst.ui.note.showAmountInteger import com.vitorpamplona.amethyst.ui.screen.NostrUserAppRecommendationsFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileBookmarksFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileConversationsFeedViewModel @@ -159,7 +160,6 @@ import com.vitorpamplona.amethyst.ui.screen.SaveableGridFeedState import com.vitorpamplona.amethyst.ui.screen.UserFeedViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.HashtagHeader -import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.showAmountAxis import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.gallery.RenderGalleryFeed import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.ShowQRDialog import com.vitorpamplona.amethyst.ui.stringRes @@ -729,7 +729,7 @@ private fun ZapTabHeader(baseUser: User) { } } - Text(text = "${showAmountAxis(zapAmount)} ${stringRes(id = R.string.zaps)}") + Text(text = "${showAmountInteger(zapAmount)} ${stringRes(id = R.string.zaps)}") } @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt index e1c66fecad..17e6df82ca 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt @@ -58,8 +58,8 @@ import androidx.core.view.WindowCompat import androidx.lifecycle.viewmodel.compose.viewModel import com.halilibo.richtext.ui.RichTextStyle import com.halilibo.richtext.ui.resolveDefaults -import com.patrykandpatrick.vico.compose.style.ChartStyle -import com.patrykandpatrick.vico.core.DefaultColors +import com.patrykandpatrick.vico.compose.common.VicoTheme +import com.patrykandpatrick.vico.compose.common.VicoTheme.CandlestickCartesianLayerColors import com.vitorpamplona.amethyst.model.ThemeType import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel @@ -441,22 +441,34 @@ val ColorScheme.largeRelayIconModifier: Modifier val ColorScheme.selectedReactionBoxModifier: Modifier get() = if (isLight) LightSelectedReactionBoxModifier else DarkSelectedReactionBoxModifier -val ColorScheme.chartStyle: ChartStyle - get() { - val defaultColors = if (isLight) DefaultColors.Light else DefaultColors.Dark - return ChartStyle.fromColors( - axisLabelColor = Color(defaultColors.axisLabelColor), - axisGuidelineColor = Color(defaultColors.axisGuidelineColor), - axisLineColor = Color(defaultColors.axisLineColor), - entityColors = - listOf( - defaultColors.entity1Color, - defaultColors.entity2Color, - defaultColors.entity3Color, - ).map(::Color), - elevationOverlayColor = Color(defaultColors.elevationOverlayColor), - ) - } +val chartLightColors = + VicoTheme( + candlestickCartesianLayerColors = + CandlestickCartesianLayerColors( + Color(0xff0ac285), + Color(0xff000000), + Color(0xffe8304f), + ), + columnCartesianLayerColors = listOf(Color(0xff3287ff), Color(0xff0ac285), Color(0xffffab02)), + lineColor = Color(0xffbcbfc2), + textColor = Color(0xff000000), + ) + +val chartDarkColors = + VicoTheme( + candlestickCartesianLayerColors = + CandlestickCartesianLayerColors( + Color(0xff0ac285), + Color(0xffffffff), + Color(0xffe8304f), + ), + columnCartesianLayerColors = listOf(Color(0xff3287ff), Color(0xff0ac285), Color(0xffffab02)), + lineColor = Color(0xff494c50), + textColor = Color(0xffffffff), + ) + +val ColorScheme.chartStyle: VicoTheme + get() = if (isLight) chartLightColors else chartDarkColors @Composable fun AmethystTheme( diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5d171da548..8d874cd70e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -46,7 +46,7 @@ torAndroid = "0.4.8.12" translate = "17.0.3" unifiedpush = "2.3.1" urlDetector = "0.1.23" -vico-charts = "1.16.0" +vico-charts = "2.0.1" zelory = "3.0.1" zoomable = "2.0.0" zxing = "3.5.3" From 6481fcbecdfbe9e65c66b2e7b5c67fca002cf28c Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Fri, 14 Feb 2025 16:07:00 +0000 Subject: [PATCH 14/76] New Crowdin translations by GitHub Action --- amethyst/src/main/res/values-cs/strings.xml | 4 ++++ amethyst/src/main/res/values-de/strings.xml | 4 ++++ amethyst/src/main/res/values-pt-rBR/strings.xml | 4 ++++ amethyst/src/main/res/values-sv-rSE/strings.xml | 4 ++++ amethyst/src/main/res/values-zh-rCN/strings.xml | 6 ++++++ 5 files changed, 22 insertions(+) diff --git a/amethyst/src/main/res/values-cs/strings.xml b/amethyst/src/main/res/values-cs/strings.xml index b2636dda1a..f9657e4c66 100644 --- a/amethyst/src/main/res/values-cs/strings.xml +++ b/amethyst/src/main/res/values-cs/strings.xml @@ -537,6 +537,8 @@ Hotovo Zjednodušené Výkonost + Klasický + Moderní Systém Světlý Tmavý @@ -551,6 +553,8 @@ Skrýt navigační panely při rolování Režim UI Zvolte styl příspěvku + Styl galerie profilu + Zvolte styl galerie Načíst obrázek Spamovací uživatelé Ztlumené. Klikněte pro odztlumení diff --git a/amethyst/src/main/res/values-de/strings.xml b/amethyst/src/main/res/values-de/strings.xml index f34afb0771..ae562e4287 100644 --- a/amethyst/src/main/res/values-de/strings.xml +++ b/amethyst/src/main/res/values-de/strings.xml @@ -542,6 +542,8 @@ anz der Bedingungen ist erforderlich Fertig Vereinfacht Leistung + Klassisch + Modern System Hell Dunkel @@ -556,6 +558,8 @@ anz der Bedingungen ist erforderlich Navigationsleisten beim Scrollen ausblenden UI-Modus Wählen Sie den Beitragsstil + Profilgalerie-Stil + Galeriestil auswählen Bild laden Spammer Stummgeschaltet. Drücken, um Ton einzuschalten diff --git a/amethyst/src/main/res/values-pt-rBR/strings.xml b/amethyst/src/main/res/values-pt-rBR/strings.xml index 7c95090245..b2ba13776d 100644 --- a/amethyst/src/main/res/values-pt-rBR/strings.xml +++ b/amethyst/src/main/res/values-pt-rBR/strings.xml @@ -537,6 +537,8 @@ Concluído Simplificado Desempenho + Clássico + Moderno Sistema Claro Escuro @@ -551,6 +553,8 @@ Ocultar as barras de navegação ao rolar Modo de interface Escolha o estilo da publicação + Estilo da Galeria de Perfil + Escolha o estilo da galeria Carregar imagem Spammers Silenciado. Clique para ativar o som diff --git a/amethyst/src/main/res/values-sv-rSE/strings.xml b/amethyst/src/main/res/values-sv-rSE/strings.xml index d2f133d1b8..c37fdfef3c 100644 --- a/amethyst/src/main/res/values-sv-rSE/strings.xml +++ b/amethyst/src/main/res/values-sv-rSE/strings.xml @@ -536,6 +536,8 @@ Slutförd Förenklad Prestanda + Klassisk + Modern System Ljus Mörk @@ -550,6 +552,8 @@ Dölj navigeringsfält vid bläddring UI läge Välj stilen för inlägg + Profilgalleri stil + Välj galleriets stil Ladda bild Spammare Ljud avstängt. Klicka för att ta bort ljudlöst diff --git a/amethyst/src/main/res/values-zh-rCN/strings.xml b/amethyst/src/main/res/values-zh-rCN/strings.xml index ed8efe43aa..4622785ca8 100644 --- a/amethyst/src/main/res/values-zh-rCN/strings.xml +++ b/amethyst/src/main/res/values-zh-rCN/strings.xml @@ -347,6 +347,7 @@ 添加到消息 添加标题 我最爱的朋友 + 使用直接链接 内容描述 日落时分,白色沙滩上的蓝色小船 打闪类型 @@ -538,6 +539,8 @@ 完整版 简化版 极速版 + 经典 + 现代 系统 浅色 深色 @@ -552,6 +555,8 @@ 滚动时隐藏导航栏 界面模式 选择帖子样式 + 资料页图库样式 + 选择图库样式 加载图像 垃圾邮件 静音。点击取消静音 @@ -810,6 +815,7 @@ 通过创建专用于关键词和标签检索的中继列表能够改善搜索结果。 设置 1 ~ 3 个支持 NIP-50 的中继,用于关键词和标签检索。 示例:\n - nostr.wine\n - reiny.nostr.band\n - reliy.noswhere.com + DM 上传 中继设置 公共时间线中继 这类中继会保存您的所有内容。Amethyst 会将您的事件发送到这些中继,其他人则通过这些中继查找您的内容。可以设置 1 ~ 3 个中继。它们可以是个人专用中继、付费中继或者公共中继。 From e921f26d5f071a40e5c89eeabdf28fbd34be3318 Mon Sep 17 00:00:00 2001 From: Dusk Date: Fri, 14 Feb 2025 13:34:59 -0400 Subject: [PATCH 15/76] fix wrong word --- amethyst/src/main/res/values-pt-rBR/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/amethyst/src/main/res/values-pt-rBR/strings.xml b/amethyst/src/main/res/values-pt-rBR/strings.xml index b2ba13776d..f1b48f0d64 100644 --- a/amethyst/src/main/res/values-pt-rBR/strings.xml +++ b/amethyst/src/main/res/values-pt-rBR/strings.xml @@ -366,7 +366,7 @@ Você não tem nenhum servidor NIP-96. Você pode usar a lista de Amethyst, ou adicionar um abaixo ↓ Você não tem nenhum servidor de Blossom configurado. Você pode usar a lista de Amethyst, ou adicionar um abaixo ↓ Servidores de Mídia Integrados - Lista padrão do Ametite. Você pode adicioná-los individualmente ou adicionar a lista. + Lista padrão do Amethyst. Você pode adicioná-los individualmente ou adicionar a lista. Usar Lista Padrão Adicionar servidor de mídia Apagar servidor de mídia From 793b3fcd1656ebdf3bd1223f25f6c465b0296470 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 14 Feb 2025 16:28:40 -0500 Subject: [PATCH 16/76] Renames PoW Parser to open space for the PoW creator --- .../{PoWRankProcessorTest.kt => PoWRankParserTest.kt} | 10 +++++----- .../java/com/vitorpamplona/quartz/nip13Pow/EventExt.kt | 4 ++-- .../nip13Pow/{PoWRankProcessor.kt => PoWRankParser.kt} | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) rename quartz/src/androidTest/java/com/vitorpamplona/quartz/nip13Pow/{PoWRankProcessorTest.kt => PoWRankParserTest.kt} (74%) rename quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/{PoWRankProcessor.kt => PoWRankParser.kt} (98%) diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip13Pow/PoWRankProcessorTest.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip13Pow/PoWRankParserTest.kt similarity index 74% rename from quartz/src/androidTest/java/com/vitorpamplona/quartz/nip13Pow/PoWRankProcessorTest.kt rename to quartz/src/androidTest/java/com/vitorpamplona/quartz/nip13Pow/PoWRankParserTest.kt index 863a2c1471..d999449ff3 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip13Pow/PoWRankProcessorTest.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip13Pow/PoWRankParserTest.kt @@ -26,24 +26,24 @@ import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) -class PoWRankProcessorTest { +class PoWRankParserTest { @Test fun setPoW() { - assertEquals(26, PoWRankProcessor.calculatePowRankOf("00000026c91e9fc75fdb95b367776e2594b931cebda6d5ca3622501006669c9e")) + assertEquals(26, PoWRankParser.calculatePowRankOf("00000026c91e9fc75fdb95b367776e2594b931cebda6d5ca3622501006669c9e")) } @Test fun setPoWIfCommited25() { - assertEquals(25, PoWRankProcessor.compute("00000026c91e9fc75fdb95b367776e2594b931cebda6d5ca3622501006669c9e", 25)) + assertEquals(25, PoWRankParser.compute("00000026c91e9fc75fdb95b367776e2594b931cebda6d5ca3622501006669c9e", 25)) } @Test fun setPoWIfCommited26() { - assertEquals(26, PoWRankProcessor.compute("00000026c91e9fc75fdb95b367776e2594b931cebda6d5ca3622501006669c9e", 26)) + assertEquals(26, PoWRankParser.compute("00000026c91e9fc75fdb95b367776e2594b931cebda6d5ca3622501006669c9e", 26)) } @Test fun setPoWIfCommited27() { - assertEquals(26, PoWRankProcessor.compute("00000026c91e9fc75fdb95b367776e2594b931cebda6d5ca3622501006669c9e", 27)) + assertEquals(26, PoWRankParser.compute("00000026c91e9fc75fdb95b367776e2594b931cebda6d5ca3622501006669c9e", 27)) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/EventExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/EventExt.kt index a38053a73d..7ad1062f75 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/EventExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/EventExt.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.quartz.nip13Pow import com.vitorpamplona.quartz.nip01Core.core.Event -fun Event.pow() = PoWRankProcessor.compute(id, tags.commitedPoW()) +fun Event.pow() = PoWRankParser.compute(id, tags.commitedPoW()) fun Event.hasPoWTag() = tags.hasPoW() @@ -34,7 +34,7 @@ fun Event.hasPoWTag() = tags.hasPoW() fun Event.strongPoWOrNull(min: Int = 20): Int? { val commitment = tags.commitedPoW() if (commitment != null) { - val pow = PoWRankProcessor.compute(id, commitment) + val pow = PoWRankParser.compute(id, commitment) if (pow >= min) { return pow } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/PoWRankProcessor.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/PoWRankParser.kt similarity index 98% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/PoWRankProcessor.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/PoWRankParser.kt index f47e1c0b4d..ae51bd962f 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/PoWRankProcessor.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/PoWRankParser.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.quartz.nip13Pow import com.vitorpamplona.quartz.nip01Core.HexKey -class PoWRankProcessor { +class PoWRankParser { companion object { @JvmStatic fun compute( From 534f1ed8cdce0b1465533b3b96bb350b576080b7 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 14 Feb 2025 16:28:56 -0500 Subject: [PATCH 17/76] Adds Tim Bourma's test case --- .../quartz/nip19Bech32/NIP19EmbedTests.kt | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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 af9a55f990..90b4f8b834 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip19Bech32/NIP19EmbedTests.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip19Bech32/NIP19EmbedTests.kt @@ -170,8 +170,20 @@ class NIP19EmbedTests { assertEquals(eyeglassesPrescriptionEvent!!.toJson(), decodedNote.toJson()) } + @Test + fun testTimsNembed() { + val uri = "nembed1r79ssq9446hkwqhl642ukmku8qg0c92pu7w3j0jyfte8tc7tvg85vmrys8x3sqgle5vjy7jpjswqhphl0kd6yf4sz0n3peyjq5rp3zkat4w6c6j3f7um0724jmfu5456xxgg2yxkn8dp23j64xsn9npcggzafyh2effyntqrqxzja8dp52kpcvc9zqxlj86e8mx05vevzxkeprjkfs4wmppxm3p96vj6yvu2mqgf5l4v99492r2qsggquxuv93uzx244652h2kkj8xseg9xkq0afpygknjtty9j4ju5v0nm9mezux9wyl6s5wr7lzce7cj397mnu0u04ha7aq3w7exelrhe3zs3l3urwa9sp36u80npllrs0hmsxqdn0fsuyav3nv0azjs5suzuurg2uymncjxez8p9xksc2j6gw992enjflgrdd7n5uq2xrpvfrd3rckw624ey0elvm6grr27tyzlf4vaswgm5vc3hdyczsl983g2j8e67r6z5zt30lat84ma4wclkwwxxrcflvdsuwd7346h7zqav4vdwe3gkt9lr87sfk4aqd2aey03tt4eyspldrqcmkx9pqe2pn63rv7grwwalr86akuldnvjm6m87wrw9sdwns8wq0rnsmj57vqwtc3g7hkwum3vl2dda78dwkycgfzw6qna3ufhpatcvq5a4hm4ehl45an8umwt0clf7rn77ctke475qglwu86hhfwhn7dkca4pkfpyc4y75rll6nvr5qc8nlhf8mk22celn5mecvyuzxd830drhdck9tcdpcafymk8wajwu2w8ha8gatggjfvq0a4jlf2sdamzj0ysqks9dk8me3q7a0qpmf6vykurkrcls4pug3u4pn4u26ezx3h8e482n07x2nsmu80dpufxqc0ttcyzhnppguxma4d8aumdawnlsyy7yzcuxl7lw5y9p4nv5h8fn6u8anpm2tsze3p6mgxy9j9uuqfxg2jvlmtjpakna5m4hln0msmw804hnun96h66fh62270yhhljnmmdl7jln07ll5vft7e870hemcld34a09n943ed6629fgtctsftma9q6tf4jfm2p0ukd2j2n2dpz53fqrkk4ctdcy2j5jar095g5jntf6u807ggkzauzt6uqkwk4tg5w7w55kskspc9663zx5dzzzfwpg3q546g2ve4kukr70n0a46eyce2crsqqq247ql5" + + val decodedNote = (Nip19Parser.uriToRoute(uri)?.entity as NEmbed).event + + assertTrue(decodedNote.verify()) + + assertEquals(timsPrescription, decodedNote.toJson()) + } + val visionPrescriptionFhir = "{\"resourceType\":\"VisionPrescription\",\"status\":\"active\",\"created\":\"2014-06-15\",\"patient\":{\"reference\":\"Patient/Donald Duck\"},\"dateWritten\":\"2014-06-15\",\"prescriber\":{\"reference\":\"Practitioner/Adam Careful\"},\"lensSpecification\":[{\"eye\":\"right\",\"sphere\":-2,\"prism\":[{\"amount\":0.5,\"base\":\"down\"}],\"add\":2},{\"eye\":\"left\",\"sphere\":-1,\"cylinder\":-0.5,\"axis\":180,\"prism\":[{\"amount\":0.5,\"base\":\"up\"}],\"add\":2}]}" val visionPrescriptionBundle = "{\"resourceType\":\"Bundle\",\"id\":\"bundle-vision-test\",\"type\":\"document\",\"entry\":[{\"resourceType\":\"Practitioner\",\"id\":\"2\",\"active\":true,\"name\":[{\"use\":\"official\",\"family\":\"Careful\",\"given\":[\"Adam\"]}],\"gender\":\"male\"},{\"resourceType\":\"Patient\",\"id\":\"1\",\"active\":true,\"name\":[{\"use\":\"official\",\"family\":\"Duck\",\"given\":[\"Donald\"]}],\"gender\":\"male\"},{\"resourceType\":\"VisionPrescription\",\"status\":\"active\",\"created\":\"2014-06-15\",\"patient\":{\"reference\":\"#1\"},\"dateWritten\":\"2014-06-15\",\"prescriber\":{\"reference\":\"#2\"},\"lensSpecification\":[{\"eye\":\"right\",\"sphere\":-2,\"prism\":[{\"amount\":0.5,\"base\":\"down\"}],\"add\":2},{\"eye\":\"left\",\"sphere\":-1,\"cylinder\":-0.5,\"axis\":180,\"prism\":[{\"amount\":0.5,\"base\":\"up\"}],\"add\":2}]}]}" val visionPrescriptionBundle2 = "{\"resourceType\":\"Bundle\",\"id\":\"bundle-vision-test\",\"type\":\"document\",\"entry\":[{\"resourceType\":\"Practitioner\",\"id\":\"2\",\"active\":true,\"name\":[{\"use\":\"official\",\"family\":\"Smith\",\"given\":[\"Dr. Joe\"]}],\"gender\":\"male\"},{\"resourceType\":\"Patient\",\"id\":\"1\",\"active\":true,\"name\":[{\"use\":\"official\",\"family\":\"Doe\",\"given\":[\"Jane\"]}],\"gender\":\"male\"},{\"resourceType\":\"VisionPrescription\",\"status\":\"active\",\"created\":\"2014-06-15\",\"patient\":{\"reference\":\"#1\"},\"dateWritten\":\"2014-06-15\",\"lensSpecification\":[{\"eye\":\"right\",\"sphere\":-2,\"prism\":[{\"amount\":0.5,\"base\":\"down\"}],\"add\":2},{\"eye\":\"left\",\"sphere\":-1,\"cylinder\":-0.5,\"axis\":180,\"prism\":[{\"amount\":0.5,\"base\":\"up\"}],\"add\":2}]}]}" + val timsPrescription = "{\"id\":\"18d8b22e6455dfc9f4c6d6be8c2cf015e961b8d160dfe5e4b7fc1578f2c4e0be\",\"pubkey\":\"46f1826abf5b03de972192e619e25fa94d775a1c555efe53a775412dbf49889b\",\"created_at\":1739566773,\"kind\":82,\"tags\":[[\"p\",\"46f1826abf5b03de972192e619e25fa94d775a1c555efe53a775412dbf49889b\"]],\"content\":\"{\\\"resourceType\\\": \\\"VisionPrescription\\\", \\\"id\\\": \\\"eyeglass-prescription-001\\\", \\\"status\\\": \\\"active\\\", \\\"created\\\": \\\"2025-02-14T10:00:00Z\\\", \\\"patient\\\": {\\\"reference\\\": \\\"Patient/12345\\\", \\\"display\\\": \\\"John Doe\\\"}, \\\"encounter\\\": {\\\"reference\\\": \\\"Encounter/67890\\\"}, \\\"dateWritten\\\": \\\"2025-02-10T15:00:00Z\\\", \\\"prescriber\\\": {\\\"reference\\\": \\\"Practitioner/56789\\\", \\\"display\\\": \\\"Dr. Emily Smith\\\"}, \\\"lensSpecification\\\": [{\\\"product\\\": {\\\"coding\\\": [{\\\"system\\\": \\\"http://terminology.hl7.org/CodeSystem/ex-visionprescriptionproduct\\\", \\\"code\\\": \\\"lens\\\", \\\"display\\\": \\\"Eyeglasses\\\"}]}, \\\"eye\\\": \\\"right\\\", \\\"sphere\\\": -2.5, \\\"cylinder\\\": -1.0, \\\"axis\\\": 180, \\\"prism\\\": [{\\\"amount\\\": 0.5, \\\"base\\\": \\\"up\\\"}], \\\"add\\\": 2.0, \\\"duration\\\": {\\\"value\\\": 24, \\\"unit\\\": \\\"months\\\", \\\"system\\\": \\\"http://unitsofmeasure.org\\\", \\\"code\\\": \\\"mo\\\"}, \\\"note\\\": [{\\\"text\\\": \\\"Right eye prescription for near-sightedness with astigmatism.\\\"}]}, {\\\"product\\\": {\\\"coding\\\": [{\\\"system\\\": \\\"http://terminology.hl7.org/CodeSystem/ex-visionprescriptionproduct\\\", \\\"code\\\": \\\"lens\\\", \\\"display\\\": \\\"Eyeglasses\\\"}]}, \\\"eye\\\": \\\"left\\\", \\\"sphere\\\": -3.0, \\\"cylinder\\\": -0.75, \\\"axis\\\": 160, \\\"prism\\\": [{\\\"amount\\\": 0.5, \\\"base\\\": \\\"down\\\"}], \\\"add\\\": 2.0, \\\"duration\\\": {\\\"value\\\": 24, \\\"unit\\\": \\\"months\\\", \\\"system\\\": \\\"http://unitsofmeasure.org\\\", \\\"code\\\": \\\"mo\\\"}, \\\"note\\\": [{\\\"text\\\": \\\"Left eye prescription for near-sightedness with astigmatism.\\\"}]}]}\",\"sig\":\"d22d3b86aea397094de8b6cdf69decdfd886c90008aeebf95fd43a2770b37d486b313bff5bdb44b78e33bbaa3f336d74ee8b36bc5b16050374054246c72d93c2\"}" } From 8022ac525f934a0b9bf423d114929b9aabd56e3c Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 14 Feb 2025 16:29:10 -0500 Subject: [PATCH 18/76] Makes sure deletions happen in the background thread. --- .../com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ac3fa4b892..dc8af5447d 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 @@ -576,7 +576,7 @@ open class NewPostViewModel : ViewModel() { } fun sendDraft(relayList: List) { - viewModelScope.launch { + viewModelScope.launch(Dispatchers.IO) { sendDraftSync(relayList) } } From 67106e8a11039c3c702b4206f6f463a9bc42555b Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 16 Feb 2025 11:53:23 -0500 Subject: [PATCH 19/76] Renames PoW processor to parser --- .../{PoWRankParserTest.kt => PoWRankProcessorTest.kt} | 10 +++++----- .../vitorpamplona/quartz/nip01Core/tags/people/PTag.kt | 6 +++++- .../java/com/vitorpamplona/quartz/nip13Pow/EventExt.kt | 4 ++-- .../nip13Pow/{PoWRankParser.kt => PoWRankProcessor.kt} | 2 +- 4 files changed, 13 insertions(+), 9 deletions(-) rename quartz/src/androidTest/java/com/vitorpamplona/quartz/nip13Pow/{PoWRankParserTest.kt => PoWRankProcessorTest.kt} (74%) rename quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/{PoWRankParser.kt => PoWRankProcessor.kt} (98%) diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip13Pow/PoWRankParserTest.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip13Pow/PoWRankProcessorTest.kt similarity index 74% rename from quartz/src/androidTest/java/com/vitorpamplona/quartz/nip13Pow/PoWRankParserTest.kt rename to quartz/src/androidTest/java/com/vitorpamplona/quartz/nip13Pow/PoWRankProcessorTest.kt index d999449ff3..863a2c1471 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip13Pow/PoWRankParserTest.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip13Pow/PoWRankProcessorTest.kt @@ -26,24 +26,24 @@ import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) -class PoWRankParserTest { +class PoWRankProcessorTest { @Test fun setPoW() { - assertEquals(26, PoWRankParser.calculatePowRankOf("00000026c91e9fc75fdb95b367776e2594b931cebda6d5ca3622501006669c9e")) + assertEquals(26, PoWRankProcessor.calculatePowRankOf("00000026c91e9fc75fdb95b367776e2594b931cebda6d5ca3622501006669c9e")) } @Test fun setPoWIfCommited25() { - assertEquals(25, PoWRankParser.compute("00000026c91e9fc75fdb95b367776e2594b931cebda6d5ca3622501006669c9e", 25)) + assertEquals(25, PoWRankProcessor.compute("00000026c91e9fc75fdb95b367776e2594b931cebda6d5ca3622501006669c9e", 25)) } @Test fun setPoWIfCommited26() { - assertEquals(26, PoWRankParser.compute("00000026c91e9fc75fdb95b367776e2594b931cebda6d5ca3622501006669c9e", 26)) + assertEquals(26, PoWRankProcessor.compute("00000026c91e9fc75fdb95b367776e2594b931cebda6d5ca3622501006669c9e", 26)) } @Test fun setPoWIfCommited27() { - assertEquals(26, PoWRankParser.compute("00000026c91e9fc75fdb95b367776e2594b931cebda6d5ca3622501006669c9e", 27)) + assertEquals(26, PoWRankProcessor.compute("00000026c91e9fc75fdb95b367776e2594b931cebda6d5ca3622501006669c9e", 27)) } } 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 index 3228a7b0a1..acdbc6266d 100644 --- 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 @@ -20,6 +20,7 @@ */ package com.vitorpamplona.quartz.nip01Core.tags.people +import android.util.Log import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Tag @@ -66,7 +67,10 @@ data class PTag( @JvmStatic fun parseKey(tag: Array): HexKey? { if (tag.isNotName(TAG_NAME, TAG_SIZE)) return null - if (tag[1].length != 64) return null + if (tag[1].length != 64) { + Log.w("PTag", "Invalid `$TAG_NAME` value ${tag.joinToString(", ")}") + return null + } return tag[1] } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/EventExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/EventExt.kt index 7ad1062f75..a38053a73d 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/EventExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/EventExt.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.quartz.nip13Pow import com.vitorpamplona.quartz.nip01Core.core.Event -fun Event.pow() = PoWRankParser.compute(id, tags.commitedPoW()) +fun Event.pow() = PoWRankProcessor.compute(id, tags.commitedPoW()) fun Event.hasPoWTag() = tags.hasPoW() @@ -34,7 +34,7 @@ fun Event.hasPoWTag() = tags.hasPoW() fun Event.strongPoWOrNull(min: Int = 20): Int? { val commitment = tags.commitedPoW() if (commitment != null) { - val pow = PoWRankParser.compute(id, commitment) + val pow = PoWRankProcessor.compute(id, commitment) if (pow >= min) { return pow } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/PoWRankParser.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/PoWRankProcessor.kt similarity index 98% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/PoWRankParser.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/PoWRankProcessor.kt index ae51bd962f..f47e1c0b4d 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/PoWRankParser.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/PoWRankProcessor.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.quartz.nip13Pow import com.vitorpamplona.quartz.nip01Core.HexKey -class PoWRankParser { +class PoWRankProcessor { companion object { @JvmStatic fun compute( From 4e2e24c7ab9ca684911e0bf23b8293ff301a5b66 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Sun, 16 Feb 2025 16:55:21 +0000 Subject: [PATCH 20/76] New Crowdin translations by GitHub Action --- amethyst/src/main/res/values-fa/strings.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/amethyst/src/main/res/values-fa/strings.xml b/amethyst/src/main/res/values-fa/strings.xml index 9b89173bc6..cc1f5b90d4 100644 --- a/amethyst/src/main/res/values-fa/strings.xml +++ b/amethyst/src/main/res/values-fa/strings.xml @@ -346,6 +346,7 @@ افزودن به پیام افزودن عنوان دوست عزیز من + استفاده از آدرس مستقیم اینترنتی توصیف محتوا قایقی آبی در ساحل شنی سفید هنگام غروب نوع زپ @@ -537,6 +538,8 @@ کامل ساده عملکرد + کلاسیک + مدرن سیستم روشن تاریک @@ -551,6 +554,8 @@ پنهان کردن نوار پیمایش هنگام مرور حالت رابط کاربر قیافه پست را انتخاب کن + سبک گالری نمایه + انتخاب سبک گالری بارگیری تصویر اسپمر بیصدا شده. برای لغو کلیک کنید @@ -904,6 +909,7 @@ خدمت در دسترس نیست - این اغلب هنگامی که یک سرور بیش از حد شلوغ است یا برای تعمیرات تعطیل است پیش می آید مهلت درگاه تمام شد - مهلت سرور در نقش یک درگاه یا پراکسی تمام شد، در انتظار پاسخ نسخه HTTP پشتیبانی نمی شود - سرور ورژن HTTP درخواست را پشتیبانی نمی کند + گونه نیز مذاکره - سرور یک مشکل پیکربندی داخلی دارد فضای ذخیره ناکافی - سرور فضای کافی برای پردازش موفق درخواست ندارد حلقه دیده شد - سرور یک حلقه بینهایت در پردازش این درخواست شناسایی کرد احراز هویت الزامی - کلاینت برای دسترسی به شبکه می بایست احراز هویت شده باشد From 08dd634eaed440ddc736391156e62b0135c843d0 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 17 Feb 2025 11:12:47 -0500 Subject: [PATCH 21/76] Fixes crash when evaluating the isHex function for an emoji --- .../src/main/java/com/vitorpamplona/quartz/utils/Hex.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/utils/Hex.kt b/quartz/src/main/java/com/vitorpamplona/quartz/utils/Hex.kt index 7f95752ad1..539a40ba6c 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/utils/Hex.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/utils/Hex.kt @@ -42,8 +42,13 @@ object Hex { if (hex.isEmpty()) return false if (hex.length and 1 != 0) return false // must be even - for (c in hex.indices) { - if (hexToByte[hex[c].code] < 0) return false + try { + for (c in hex.indices) { + if (hexToByte[hex[c].code] < 0) return false + } + } catch (e: IllegalArgumentException) { + // there are p tags with emoji's which makes the hex[c].code > 256 + return false } return true From 41e47fa71db04bce532091772ae91407ad6199b4 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 17 Feb 2025 11:17:38 -0500 Subject: [PATCH 22/76] Fixes running new events on the main thread. --- .../amethyst/ui/screen/loggedIn/NewPostScreen.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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 bf4295c85e..7b3d946848 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 @@ -193,6 +193,7 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.lang.Math.round @OptIn(ExperimentalMaterial3Api::class, FlowPreview::class) @@ -325,8 +326,10 @@ fun NewPostScreen( CloseButton( onPress = { scope.launch { - postViewModel.sendDraftSync(relayList = relayList) - postViewModel.cancel() + withContext(Dispatchers.IO) { + postViewModel.sendDraftSync(relayList = relayList) + postViewModel.cancel() + } delay(100) nav.popBack() } From 7ad8b2ce46f47fde0c1475e9afe706e06ac62fd5 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 17 Feb 2025 14:12:54 -0500 Subject: [PATCH 23/76] Migrates LocalCache from being based on ATags to Addresses, removing the need to use memory space to store relay hints --- .../vitorpamplona/amethyst/model/Account.kt | 68 +++++++------------ .../vitorpamplona/amethyst/model/Channel.kt | 30 +++++--- .../amethyst/model/LocalCache.kt | 59 ++++++++-------- .../com/vitorpamplona/amethyst/model/Note.kt | 31 ++++----- .../amethyst/model/ThreadAssembler.kt | 10 +-- .../service/NostrCommunityDataSource.kt | 2 +- .../service/NostrSingleEventDataSource.kt | 4 +- .../amethyst/ui/actions/NewPostViewModel.kt | 6 +- .../ui/dal/DiscoverCommunityFeedFilter.kt | 9 ++- .../amethyst/ui/dal/FilterByListParams.kt | 14 ++-- ...UserProfileAppRecommendationsFeedFilter.kt | 3 +- .../amethyst/ui/navigation/DrawerContent.kt | 16 ++--- .../vitorpamplona/amethyst/ui/note/Loaders.kt | 12 ++-- .../ui/note/NIP05VerificationDisplay.kt | 4 +- .../amethyst/ui/note/NoteQuickActionMenu.kt | 8 +-- .../ui/note/ShowEmojiSuggestionList.kt | 4 +- .../ui/note/UpdateReactionTypeDialog.kt | 16 ++--- .../amethyst/ui/note/elements/ForkInfo.kt | 7 +- .../amethyst/ui/note/types/Emoji.kt | 10 +-- .../amethyst/ui/note/types/Git.kt | 8 +-- .../amethyst/ui/note/types/Highlight.kt | 10 +-- .../ui/note/types/InteractiveStory.kt | 4 +- .../ui/note/types/RenderPostApproval.kt | 2 +- .../amethyst/ui/screen/FollowListState.kt | 2 +- .../ui/screen/loggedIn/AccountViewModel.kt | 24 ++++--- .../loggedIn/chatrooms/ChannelScreen.kt | 6 +- .../screen/loggedIn/profile/ProfileScreen.kt | 8 +-- .../loggedIn/threadview/ThreadFeedView.kt | 2 +- .../quartz/blossom/BlossomServersEvent.kt | 3 + .../edits/PrivateOutboxRelayListEvent.kt | 3 + .../forks/BaseThreadedEventExt.kt | 6 +- .../InteractiveStoryReadingStateEvent.kt | 8 ++- .../interactiveStories/tags/StoryOptionTag.kt | 17 +++-- .../nip01Core/core/BaseAddressableEvent.kt | 2 +- .../nip01Core/core/BaseReplaceableEvent.kt | 2 +- .../nip01Core/tags/addressables/ATag.kt | 17 ++--- .../nip01Core/tags/addressables/ATagParser.kt | 9 +++ .../nip01Core/tags/addressables/Address.kt | 2 +- .../nip01Core/tags/addressables/EventExt.kt | 4 ++ .../tags/addressables/TagArrayExt.kt | 12 ++-- .../quartz/nip09Deletions/DeletionEvent.kt | 2 +- .../quartz/nip10Notes/BaseThreadedEvent.kt | 4 +- .../settings/ChatMessageRelayListEvent.kt | 3 + .../quartz/nip19Bech32/entities/NAddress.kt | 6 +- .../quartz/nip19Bech32/entities/NEvent.kt | 6 +- .../nip23LongContent/LongTextNoteEvent.kt | 2 +- .../selection/EmojiPackSelectionEvent.kt | 8 ++- .../quartz/nip34Git/issue/GitIssueEvent.kt | 4 +- .../quartz/nip34Git/patch/GitPatchEvent.kt | 10 +++ .../quartz/nip34Git/reply/GitReplyEvent.kt | 2 +- .../nip50Search/SearchRelayListEvent.kt | 3 + .../quartz/nip51Lists/GeneralListEvent.kt | 18 ++--- .../quartz/nip51Lists/MuteListEvent.kt | 5 +- .../quartz/nip51Lists/PeopleListEvent.kt | 3 + .../chat/LiveActivitiesChatMessageEvent.kt | 4 +- .../quartz/nip54Wiki/WikiNoteEvent.kt | 2 +- .../quartz/nip58Badges/BadgeProfilesEvent.kt | 3 + .../AdvertisedRelayListEvent.kt | 3 + .../approval/CommunityPostApprovalEvent.kt | 7 +- .../nip78AppData/AppSpecificDataEvent.kt | 6 ++ .../quartz/nip84Highlights/HighlightEvent.kt | 5 +- .../config/FileServersEvent.kt | 3 + 62 files changed, 307 insertions(+), 266 deletions(-) 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 d6091f0a19..5907b79d11 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -85,8 +85,10 @@ 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.isTaggedAddressableNote +import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedATags import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedAddresses +import com.vitorpamplona.quartz.nip01Core.tags.events.isTaggedEvent import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEventIds import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohash import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohashes @@ -1001,7 +1003,7 @@ class Account( hashtags = (listEvent.hashtags() + listEvent.filterHashtags(privateTagList)).toSet(), geotags = (listEvent.geohashes() + listEvent.filterGeohashes(privateTagList)).toSet(), addresses = - (listEvent.taggedAddresses() + listEvent.filterAddresses(privateTagList)) + (listEvent.taggedATags() + listEvent.filterATags(privateTagList)) .map { it.toTag() } .toSet(), ), @@ -1124,7 +1126,9 @@ class Account( fun getEmojiPackSelectionFlow(): StateFlow = getEmojiPackSelectionNote().flow().metadata.stateFlow - fun getEmojiPackSelectionNote(): AddressableNote = LocalCache.getOrCreateAddressableNote(EmojiPackSelectionEvent.createAddressATag(userProfile().pubkeyHex)) + fun getEmojiPackSelectionAddress() = EmojiPackSelectionEvent.createAddress(userProfile().pubkeyHex) + + fun getEmojiPackSelectionNote(): AddressableNote = LocalCache.getOrCreateAddressableNote(getEmojiPackSelectionAddress()) fun convertEmojiSelectionPack(selection: EmojiPackSelectionEvent?): List>? = selection?.taggedAddresses()?.map { @@ -1849,7 +1853,7 @@ class Account( val contactList = userProfile().latestContactList if (contactList != null) { - ContactListEvent.followAddressableEvent(contactList, community.address, signer) { + ContactListEvent.followAddressableEvent(contactList, community.toATag(), signer) { Amethyst.instance.client.send(it) LocalCache.justConsume(it, null) } @@ -1862,7 +1866,7 @@ class Account( followUsers = emptyList(), followTags = emptyList(), followGeohashes = emptyList(), - followCommunities = listOf(community.address), + followCommunities = listOf(community.toATag()), followEvents = DefaultChannels.toList(), relayUse = relays, signer = signer, @@ -2008,7 +2012,7 @@ class Account( if (contactList != null && contactList.tags.isNotEmpty()) { ContactListEvent.unfollowAddressableEvent( contactList, - community.address, + community.toATag(), signer, onReady = this::onNewEventCreated, ) @@ -2860,7 +2864,7 @@ class Account( if (note is AddressableNote) { BookmarkListEvent.addReplaceable( userProfile().latestBookmarkList, - note.address, + note.toATag(), isPrivate, signer, ) { @@ -2891,7 +2895,7 @@ class Account( if (note is AddressableNote) { BookmarkListEvent.removeReplaceable( bookmarks, - note.address, + note.toATag(), isPrivate, signer, ) { @@ -2954,7 +2958,7 @@ class Account( } if (note is AddressableNote) { - userProfile().latestBookmarkList?.privateTaggedAddresses(signer) { + userProfile().latestBookmarkList?.privateAddress(signer) { onReady(it.contains(note.address)) } } else { @@ -2968,40 +2972,19 @@ class Account( if (!isWriteable()) return false if (note is AddressableNote) { - return userProfile().latestBookmarkList?.taggedAddresses()?.contains(note.address) == true + return userProfile().latestBookmarkList?.isTaggedAddressableNote(note.idHex) == true } else { - return userProfile().latestBookmarkList?.taggedEventIds()?.contains(note.idHex) == true + return userProfile().latestBookmarkList?.isTaggedEvent(note.idHex) == true } } - fun getAppSpecificDataNote(): AddressableNote { - val aTag = AppSpecificDataEvent.createTag(userProfile().pubkeyHex, APP_SPECIFIC_DATA_D_TAG) - return LocalCache.getOrCreateAddressableNote(aTag) - } + fun getAppSpecificDataNote() = LocalCache.getOrCreateAddressableNote(AppSpecificDataEvent.createAddress(userProfile().pubkeyHex, APP_SPECIFIC_DATA_D_TAG)) fun getAppSpecificDataFlow(): StateFlow = getAppSpecificDataNote().flow().metadata.stateFlow - fun getBlockListNote(): AddressableNote { - val aTag = - ATag( - PeopleListEvent.KIND, - userProfile().pubkeyHex, - PeopleListEvent.BLOCK_LIST_D_TAG, - null, - ) - return LocalCache.getOrCreateAddressableNote(aTag) - } + fun getBlockListNote() = LocalCache.getOrCreateAddressableNote(PeopleListEvent.createBlockAddress(userProfile().pubkeyHex)) - fun getMuteListNote(): AddressableNote { - val aTag = - ATag( - MuteListEvent.KIND, - userProfile().pubkeyHex, - "", - null, - ) - return LocalCache.getOrCreateAddressableNote(aTag) - } + fun getMuteListNote() = LocalCache.getOrCreateAddressableNote(MuteListEvent.createAddress(userProfile().pubkeyHex)) fun getMuteListFlow(): StateFlow = getMuteListNote().flow().metadata.stateFlow @@ -3345,10 +3328,7 @@ class Account( ) } - fun getDMRelayListNote(): AddressableNote = - LocalCache.getOrCreateAddressableNote( - ChatMessageRelayListEvent.createAddressATag(signer.pubKey), - ) + fun getDMRelayListNote(): AddressableNote = LocalCache.getOrCreateAddressableNote(ChatMessageRelayListEvent.createAddress(signer.pubKey)) fun getDMRelayListFlow(): StateFlow = getDMRelayListNote().flow().metadata.stateFlow @@ -3380,7 +3360,7 @@ class Account( fun getPrivateOutboxRelayListNote(): AddressableNote = LocalCache.getOrCreateAddressableNote( - PrivateOutboxRelayListEvent.createAddressATag(signer.pubKey), + PrivateOutboxRelayListEvent.createAddress(signer.pubKey), ) fun getPrivateOutboxRelayListFlow(): StateFlow = getPrivateOutboxRelayListNote().flow().metadata.stateFlow @@ -3414,7 +3394,7 @@ class Account( fun getSearchRelayListNote(): AddressableNote = LocalCache.getOrCreateAddressableNote( - SearchRelayListEvent.createAddressATag(signer.pubKey), + SearchRelayListEvent.createAddress(signer.pubKey), ) fun getSearchRelayListFlow(): StateFlow = getSearchRelayListNote().flow().metadata.stateFlow @@ -3448,7 +3428,7 @@ class Account( fun getNIP65RelayListNote(pubkey: HexKey = signer.pubKey): AddressableNote = LocalCache.getOrCreateAddressableNote( - AdvertisedRelayListEvent.createAddressATag(pubkey), + AdvertisedRelayListEvent.createAddress(pubkey), ) fun getNIP65RelayListFlow(pubkey: HexKey = signer.pubKey): StateFlow = getNIP65RelayListNote(pubkey).flow().metadata.stateFlow @@ -3484,13 +3464,13 @@ class Account( fun getFileServersListFlow(): StateFlow = getFileServersNote().flow().metadata.stateFlow - fun getFileServersNote(): AddressableNote = LocalCache.getOrCreateAddressableNote(FileServersEvent.createAddressATag(userProfile().pubkeyHex)) + fun getFileServersNote(): AddressableNote = LocalCache.getOrCreateAddressableNote(FileServersEvent.createAddress(userProfile().pubkeyHex)) fun getBlossomServersList(): BlossomServersEvent? = getBlossomServersNote().event as? BlossomServersEvent fun getBlossomServersListFlow(): StateFlow = getBlossomServersNote().flow().metadata.stateFlow - fun getBlossomServersNote(): AddressableNote = LocalCache.getOrCreateAddressableNote(BlossomServersEvent.createAddressATag(userProfile().pubkeyHex)) + fun getBlossomServersNote(): AddressableNote = LocalCache.getOrCreateAddressableNote(BlossomServersEvent.createAddress(userProfile().pubkeyHex)) fun host(url: String): String = try { 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 09f4b95270..ded1755e69 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt @@ -32,6 +32,8 @@ 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.nip01Core.tags.addressables.Address +import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress import com.vitorpamplona.quartz.nip19Bech32.toNAddr import com.vitorpamplona.quartz.nip19Bech32.toNEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent @@ -81,11 +83,11 @@ class PublicChatChannel( @Stable class LiveActivitiesChannel( - val address: ATag, -) : Channel(address.toTag()) { + val address: Address, +) : Channel(address.toValue()) { var info: LiveActivitiesEvent? = null - override fun idNote() = address.toNAddr() + override fun idNote() = toNAddr() override fun idDisplayNote() = idNote().toShortenHex() @@ -93,6 +95,8 @@ class LiveActivitiesChannel( override fun relays() = info?.allRelayUrls() ?: super.relays() + fun relayHintUrl() = relays().firstOrNull() + fun updateChannelInfo( creator: User, channelInfo: LiveActivitiesEvent, @@ -112,6 +116,10 @@ class LiveActivitiesChannel( listOfNotNull(info?.title(), info?.summary()) .filter { it.contains(prefix, true) } .isNotEmpty() + + fun toNAddr() = NAddress.create(address.kind, address.pubKeyHex, address.dTag, relayHintUrl()) + + fun toATag() = ATag(address, relayHintUrl()) } data class Counter( @@ -167,6 +175,15 @@ abstract class Channel( } } + fun addRelay(relay: Relay) { + val counter = relays[relay.brief] + if (counter != null) { + counter.number++ + } else { + addRelaySync(relay.brief) + } + } + fun addNote( note: Note, relay: Relay? = null, @@ -178,12 +195,7 @@ abstract class Channel( } if (relay != null) { - val counter = relays[relay.brief] - if (counter != null) { - counter.number++ - } else { - addRelaySync(relay.brief) - } + addRelay(relay) } } 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 3f76234395..51047d5fcc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -55,6 +55,7 @@ import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.tagValueContains import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip01Core.tags.addressables.mapTaggedAddress import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedAddresses import com.vitorpamplona.quartz.nip01Core.tags.events.ETag @@ -275,6 +276,8 @@ object LocalCache { fun getAddressableNoteIfExists(key: String): AddressableNote? = addressables.get(key) + fun getAddressableNoteIfExists(address: Address): AddressableNote? = getAddressableNoteIfExists(address.toValue()) + fun getNoteIfExists(key: String): Note? = addressables.get(key) ?: notes.get(key) fun getNoteIfExists(key: ETag): Note? = notes.get(key.eventId) @@ -290,7 +293,7 @@ object LocalCache { fun getOrCreateNote(event: Event): Note = if (event is AddressableEvent) { - getOrCreateAddressableNote(event.aTag()) + getOrCreateAddressableNote(event.address()) } else { getOrCreateNote(event.id) } @@ -304,8 +307,6 @@ object LocalCache { return null } - fun checkGetOrCreateNote(atag: ATag) = getOrCreateAddressableNote(atag) - fun checkGetOrCreateNote(key: String): Note? { checkNotInMainThread() @@ -371,9 +372,10 @@ object LocalCache { if (isValidHex(key)) { return channels.getOrCreate(key) { PublicChatChannel(key) } } - val aTag = ATag.parse(key, null) - if (aTag != null) { - return channels.getOrCreate(aTag.toTag()) { LiveActivitiesChannel(aTag) } + + val address = Address.parse(key) + if (address != null) { + return channels.getOrCreate(address.toValue()) { LiveActivitiesChannel(address) } } return null } @@ -387,7 +389,7 @@ object LocalCache { fun checkGetOrCreateAddressableNote(key: String): AddressableNote? = try { - val addr = ATag.parse(key, null) // relay doesn't matter for the index. + val addr = Address.parse(key) if (addr != null) { getOrCreateAddressableNote(addr) } else { @@ -398,17 +400,12 @@ object LocalCache { null } - fun getOrCreateAddressableNoteInternal(key: ATag): AddressableNote { - // checkNotInMainThread() - - // we can't use naddr here because naddr might include relay info and - // the preferred relay should not be part of the index. - return addressables.getOrCreate(key.toTag()) { + fun getOrCreateAddressableNoteInternal(key: Address): AddressableNote = + addressables.getOrCreate(key.toValue()) { AddressableNote(key) } - } - fun getOrCreateAddressableNote(key: ATag): AddressableNote { + fun getOrCreateAddressableNote(key: Address): AddressableNote { val note = getOrCreateAddressableNoteInternal(key) // Loads the user outside a Syncronized block to avoid blocking if (note.author == null) { @@ -598,7 +595,7 @@ object LocalCache { relay: Relay?, ) { val version = getOrCreateNote(event.id) - val note = getOrCreateAddressableNote(event.aTag()) + val note = getOrCreateAddressableNote(event.address()) val author = getOrCreateUser(event.pubKey) if (version.event == null) { @@ -632,7 +629,7 @@ object LocalCache { relay: Relay?, ) { val version = getOrCreateNote(event.id) - val note = getOrCreateAddressableNote(event.aTag()) + val note = getOrCreateAddressableNote(event.address()) val author = getOrCreateUser(event.pubKey) if (version.event == null) { @@ -693,7 +690,7 @@ object LocalCache { event.taggedAddresses().map { getOrCreateAddressableNote(it) } is CommunityPostApprovalEvent -> event.approvedEvents().mapNotNull { checkGetOrCreateNote(it) } + - event.approvedAddresses().map { checkGetOrCreateNote(it) } + event.approvedAddresses().map { getOrCreateAddressableNote(it) } is ReactionEvent -> event.originalPost().mapNotNull { checkGetOrCreateNote(it) } + event.taggedAddresses().map { getOrCreateAddressableNote(it) } @@ -730,7 +727,7 @@ object LocalCache { relay: Relay?, ) { val version = getOrCreateNote(event.id) - val note = getOrCreateAddressableNote(event.aTag()) + val note = getOrCreateAddressableNote(event.address()) val author = getOrCreateUser(event.pubKey) if (version.event == null) { @@ -743,9 +740,11 @@ object LocalCache { if (event.createdAt > (note.createdAt() ?: 0)) { note.loadEvent(event, author, emptyList()) - val channel = - getOrCreateChannel(note.idHex) { LiveActivitiesChannel(note.address) } - as? LiveActivitiesChannel + val channel = getOrCreateChannel(note.idHex) { LiveActivitiesChannel(note.address) } as? LiveActivitiesChannel + + if (relay != null) { + channel?.addRelay(relay) + } val creator = event.host()?.let { checkGetOrCreateUser(it.pubKey) } ?: author @@ -900,7 +899,7 @@ object LocalCache { relay: Relay?, ) { val version = getOrCreateNote(event.id) - val note = getOrCreateAddressableNote(event.aTag()) + val note = getOrCreateAddressableNote(event.address()) val author = getOrCreateUser(event.pubKey) if (version.event == null) { @@ -957,7 +956,7 @@ object LocalCache { fun consume(event: BadgeProfilesEvent) { val version = getOrCreateNote(event.id) - val note = getOrCreateAddressableNote(event.aTag()) + val note = getOrCreateAddressableNote(event.address()) val author = getOrCreateUser(event.pubKey) if (version.event == null) { @@ -1029,7 +1028,7 @@ object LocalCache { relay: Relay?, ) { val version = getOrCreateNote(event.id) - val note = getOrCreateAddressableNote(event.aTag()) + val note = getOrCreateAddressableNote(event.address()) val author = getOrCreateUser(event.pubKey) val replyTos = computeReplyTo(event) @@ -1131,7 +1130,7 @@ object LocalCache { } } - val addressList = event.deleteAddressTags() + val addressList = event.deleteAddressIds() val addressSet = addressList.toSet() addressList @@ -1296,7 +1295,7 @@ object LocalCache { val author = getOrCreateUser(event.pubKey) - val communities = event.communities() + val communities = event.communityAddresses() val eventsApproved = computeReplyTo(event) val repliesTo = communities.map { getOrCreateAddressableNote(it) } @@ -1470,9 +1469,9 @@ object LocalCache { event: LiveActivitiesChatMessageEvent, relay: Relay?, ) { - val activityId = event.activity() ?: return + val activityAddress = event.activityAddress() ?: return - val channel = getOrCreateChannel(activityId.toTag()) { LiveActivitiesChannel(activityId) } + val channel = getOrCreateChannel(activityAddress.toValue()) { LiveActivitiesChannel(activityAddress) } val note = getOrCreateNote(event.id) channel.addNote(note, relay) @@ -2348,7 +2347,7 @@ object LocalCache { draftWrap: DraftEvent, draft: Event, ) { - val note = getOrCreateAddressableNote(draftWrap.aTag()) + val note = getOrCreateAddressableNote(draftWrap.address()) val author = getOrCreateUser(draftWrap.pubKey) when (draft) { 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 3d370479c8..5d1f1d9db9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -46,6 +46,7 @@ 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.addressables.Address import com.vitorpamplona.quartz.nip01Core.tags.events.ETag import com.vitorpamplona.quartz.nip01Core.tags.events.EventReference import com.vitorpamplona.quartz.nip01Core.tags.hashtags.anyHashTag @@ -54,6 +55,7 @@ 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.NAddress import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent import com.vitorpamplona.quartz.nip19Bech32.toNAddr import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent @@ -85,11 +87,11 @@ import kotlin.coroutines.resume @Stable class AddressableNote( - val address: ATag, -) : Note(address.toTag()) { - override fun idNote() = address.toNAddr(relayHintUrl()) + val address: Address, +) : Note(address.toValue()) { + override fun idNote() = toNAddr() - override fun toNEvent() = address.toNAddr(relayHintUrl()) + override fun toNEvent() = toNAddr() override fun idDisplayNote() = idNote().toShortenHex() @@ -108,13 +110,15 @@ class AddressableNote( override fun wasOrShouldBeDeletedBy( deletionEvents: Set, - deletionAddressables: Set, + deletionAddressables: Set
, ): Boolean { val thisEvent = event return deletionAddressables.contains(address) || (thisEvent != null && deletionEvents.contains(thisEvent.id)) } - override fun toATag() = ATag.parse(idHex, relayHintUrl()) + fun toNAddr() = NAddress.create(address.kind, address.pubKeyHex, address.dTag, relayHintUrl()) + + fun toATag() = ATag(address, relayHintUrl()) } @Stable @@ -211,7 +215,7 @@ open class Note( null } - open fun address(): ATag? = null + open fun address(): Address? = null open fun createdAt() = event?.createdAt @@ -915,10 +919,10 @@ open class Note( open fun wasOrShouldBeDeletedBy( deletionEvents: Set, - deletionAddressables: Set, + deletionAddressables: Set
, ): Boolean { val thisEvent = event - return deletionEvents.contains(idHex) || (thisEvent is AddressableEvent && deletionAddressables.contains(thisEvent.aTag())) + return deletionEvents.contains(idHex) || (thisEvent is AddressableEvent && deletionAddressables.contains(thisEvent.address())) } fun toETag(): ETag { @@ -950,15 +954,6 @@ open class Note( 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 - } - } } @Stable 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 b89437a33a..0a7b9e5b03 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ThreadAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ThreadAssembler.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.model import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent -import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent import kotlinx.collections.immutable.ImmutableSet @@ -132,7 +132,7 @@ class ThreadAssembler { } class OnlyLatestVersionSet : MutableSet { - val map = hashMapOf() + val map = hashMapOf() val set = hashSetOf() override fun add(element: Note): Boolean { @@ -142,14 +142,14 @@ class OnlyLatestVersionSet : MutableSet { return if (element is AddressableNote && loadedCreatedAt != null) { innerAdd(element.address, element, loadedCreatedAt) } else if (noteEvent is AddressableEvent && loadedCreatedAt != null) { - innerAdd(noteEvent.aTag(), element, loadedCreatedAt) + innerAdd(noteEvent.address(), element, loadedCreatedAt) } else { set.add(element) } } private fun innerAdd( - address: ATag, + address: Address, element: Note, loadedCreatedAt: Long, ): Boolean { @@ -193,7 +193,7 @@ class OnlyLatestVersionSet : MutableSet { element.address()?.let { map.remove(it) } - (element.event as? AddressableEvent)?.aTag()?.let { + (element.event as? AddressableEvent)?.address()?.let { map.remove(it) } 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 a7c5ce4513..70e74ee5bd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrCommunityDataSource.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrCommunityDataSource.kt @@ -50,7 +50,7 @@ object NostrCommunityDataSource : AmethystNostrDataSource("SingleCommunityFeed") authors = authors, tags = mapOf( - "a" to listOf(myCommunityToWatch.address.toTag()), + "a" to listOf(myCommunityToWatch.address.toValue()), ), kinds = listOf(CommunityPostApprovalEvent.KIND), limit = 500, 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 a62e423ddb..7ba8259dac 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt @@ -81,7 +81,7 @@ object NostrSingleEventDataSource : AmethystNostrDataSource("SingleEventFeed") { CommunityPostApprovalEvent.KIND, LiveActivitiesChatMessageEvent.KIND, ), - tags = mapOf("a" to it.mapNotNull { it.address()?.toTag() }), + tags = mapOf("a" to it.mapNotNull { it.address()?.toValue() }), since = findMinimumEOSEs(it), // Max amount of "replies" to download on a specific event. limit = 1000, @@ -95,7 +95,7 @@ object NostrSingleEventDataSource : AmethystNostrDataSource("SingleEventFeed") { listOf( DeletionEvent.KIND, ), - tags = mapOf("a" to it.mapNotNull { it.address()?.toTag() }), + tags = mapOf("a" to it.mapNotNull { it.address()?.toValue() }), since = findMinimumEOSEs(it), // Max amount of "replies" to download on a specific event. limit = 10, 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 dc8af5447d..3f5be78752 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 @@ -792,11 +792,7 @@ open class NewPostViewModel : ViewModel() { imetas(usedAttachments) } } else { - if (channel.address.relay == null) { - channel.address.relay = channelRelays.firstOrNull() ?: replyingToEvent?.relay - } - - LiveActivitiesChatMessageEvent.message(tagger.message, channel.address) { + LiveActivitiesChatMessageEvent.message(tagger.message, channel.toATag()) { tagger.pTags?.let { notify(it.map { it.toPTag() }) } hashtags(findHashtags(tagger.message)) 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 e5584738e9..6737d99be9 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 @@ -23,8 +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.nip01Core.tags.addressables.ATag -import com.vitorpamplona.quartz.nip19Bech32.parseAtagUnckecked +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip51Lists.MuteListEvent import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent @@ -54,7 +53,7 @@ open class DiscoverCommunityFeedFilter( val notes = LocalCache.addressables.mapNotNullIntoSet { key, note -> val noteEvent = note.event - if (noteEvent == null && shouldInclude(ATag.parseAtagUnckecked(key), filterParams)) { + if (noteEvent == null && shouldInclude(Address.parse(key), filterParams)) { // send unloaded communities to the screen note } else if (noteEvent is CommunityDefinitionEvent && filterParams.match(noteEvent)) { @@ -86,7 +85,7 @@ open class DiscoverCommunityFeedFilter( if (noteEvent is CommunityDefinitionEvent && filterParams.match(noteEvent)) { listOf(note) } else if (noteEvent is CommunityPostApprovalEvent) { - noteEvent.communities().mapNotNull { + noteEvent.communityAddresses().mapNotNull { val definitionNote = LocalCache.getOrCreateAddressableNote(it) val definitionEvent = definitionNote.event @@ -106,7 +105,7 @@ open class DiscoverCommunityFeedFilter( } private fun shouldInclude( - aTag: ATag?, + aTag: Address?, params: FilterByListParams, ) = aTag != null && aTag.kind == CommunityDefinitionEvent.KIND && params.match(aTag) 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 49e3e57153..4f3d850b59 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 @@ -24,7 +24,7 @@ import com.vitorpamplona.amethyst.model.AROUND_ME import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip01Core.tags.addressables.isTaggedAddressableNotes import com.vitorpamplona.quartz.nip01Core.tags.geohash.isTaggedGeoHashes import com.vitorpamplona.quartz.nip01Core.tags.hashtags.isTaggedHashes @@ -68,10 +68,10 @@ class FilterByListParams( } } - fun isATagInList(aTag: ATag): Boolean { + fun isAuthorInFollows(address: Address): Boolean { if (followLists == null) return false - return aTag.pubKeyHex in followLists.authors + return address.pubKeyHex in followLists.authors } fun match( @@ -81,10 +81,10 @@ class FilterByListParams( (isHiddenList || isNotHidden(noteEvent.pubKey)) && isNotInTheFuture(noteEvent) - fun match(aTag: ATag?) = - aTag != null && - (isGlobal || isATagInList(aTag)) && - (isHiddenList || isNotHidden(aTag.pubKeyHex)) + fun match(address: Address?) = + address != null && + (isGlobal || isAuthorInFollows(address)) && + (isHiddenList || isNotHidden(address.pubKeyHex)) companion object { fun showHiddenKey( 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 a6b515f5bb..af5b729fc3 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,6 @@ 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.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip89AppHandlers.recommendation.AppRecommendationEvent class UserProfileAppRecommendationsFeedFilter( @@ -48,7 +47,7 @@ class UserProfileAppRecommendationsFeedFilter( val noteEvent = it.event if (noteEvent is AppRecommendationEvent) { if (noteEvent.pubKey == user.pubkeyHex) { - return noteEvent.recommendations().map { LocalCache.getOrCreateAddressableNote(ATag(it.address)) } + return noteEvent.recommendations().map { LocalCache.getOrCreateAddressableNote(it.address) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt index 817aeb1ec9..c7f924a690 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt @@ -113,7 +113,7 @@ import com.vitorpamplona.amethyst.ui.theme.profileContentHeaderModifier import com.vitorpamplona.amethyst.ui.tor.ConnectTorDialog import com.vitorpamplona.ammolite.relays.RelayPoolStatus 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.nip02FollowList.ImmutableListOfLists @Composable @@ -278,7 +278,7 @@ private fun EditStatusBoxes( @Composable fun StatusEditBar( savedStatus: String? = null, - tag: ATag? = null, + address: Address? = null, accountViewModel: AccountViewModel, nav: INav, ) { @@ -311,10 +311,10 @@ fun StatusEditBar( keyboardActions = KeyboardActions( onSend = { - if (tag == null) { + if (address == null) { accountViewModel.createStatus(currentStatus.value) } else { - accountViewModel.updateStatus(tag, currentStatus.value) + accountViewModel.updateStatus(address, currentStatus.value) } focusManager.clearFocus(true) @@ -324,17 +324,17 @@ fun StatusEditBar( trailingIcon = { if (hasChanged.value) { SendButton { - if (tag == null) { + if (address == null) { accountViewModel.createStatus(currentStatus.value) } else { - accountViewModel.updateStatus(tag, currentStatus.value) + accountViewModel.updateStatus(address, currentStatus.value) } focusManager.clearFocus(true) } } else { - if (tag != null) { + if (address != null) { UserStatusDeleteButton { - accountViewModel.deleteStatus(tag) + accountViewModel.deleteStatus(address) focusManager.clearFocus(true) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt index 556b522d47..b49180a090 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt @@ -39,7 +39,7 @@ import com.vitorpamplona.amethyst.service.CachedGeoLocations import com.vitorpamplona.amethyst.ui.components.GenericLoadable import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists -import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.Dispatchers @@ -112,20 +112,20 @@ fun LoadAddressableNote( @Composable fun LoadAddressableNote( - aTag: ATag, + address: Address, accountViewModel: AccountViewModel, content: @Composable (AddressableNote?) -> Unit, ) { var note by - remember(aTag) { - mutableStateOf(accountViewModel.getAddressableNoteIfExists(aTag.toTag())) + remember(address) { + mutableStateOf(accountViewModel.getAddressableNoteIfExists(address.toValue())) } if (note == null) { - LaunchedEffect(key1 = aTag) { + LaunchedEffect(key1 = address) { val newNote = withContext(Dispatchers.IO) { - accountViewModel.getOrCreateAddressableNote(aTag) + accountViewModel.getOrCreateAddressableNote(address) } if (note != newNote) { note = newNote 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 78f5b10e0c..f0a4770156 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 @@ -73,7 +73,7 @@ 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.metadata.UserMetadata -import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip01Core.tags.addressables.firstTaggedAddress import com.vitorpamplona.quartz.nip01Core.tags.events.ETag import com.vitorpamplona.quartz.nip01Core.tags.events.firstTaggedEvent @@ -276,7 +276,7 @@ fun DisplayStatusInner( content: String, type: String, url: String?, - nostrATag: ATag?, + nostrATag: Address?, nostrETag: ETag?, accountViewModel: AccountViewModel, nav: INav, 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 7aec26e140..49c617e1cf 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 @@ -125,13 +125,13 @@ val njumpLink = { nip19BechAddress: String -> val externalLinkForNote = { note: Note -> if (note is AddressableNote) { if (note.event?.bountyBaseReward() != null) { - "https://nostrbounties.com/b/${note.address().toNAddr()}" + "https://nostrbounties.com/b/${note.toNAddr()}" } else if (note.event is PeopleListEvent) { - "https://listr.lol/a/${note.address().toNAddr()}" + "https://listr.lol/a/${note.toNAddr()}" } else if (note.event is AudioTrackEvent) { - "https://zapstr.live/?track=${note.address().toNAddr()}" + "https://zapstr.live/?track=${note.toNAddr()}" } else { - njumpLink(note.address().toNAddr()) + njumpLink(note.toNAddr()) } } else { if (note.event is FileHeaderEvent) { 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 a6e229c3f0..fa5a6bfb5c 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 @@ -61,7 +61,7 @@ import kotlinx.coroutines.flow.Flow @Composable fun WatchAndLoadMyEmojiList(accountViewModel: AccountViewModel) { LoadAddressableNote( - EmojiPackSelectionEvent.createAddressATag(accountViewModel.userProfile().pubkeyHex), + EmojiPackSelectionEvent.createAddress(accountViewModel.userProfile().pubkeyHex), accountViewModel, ) { emptyNote -> emptyNote?.let { usersEmojiList -> @@ -73,7 +73,7 @@ fun WatchAndLoadMyEmojiList(accountViewModel: AccountViewModel) { .observeAsState((usersEmojiList.event as? EmojiPackSelectionEvent)?.taggedAddresses()?.toImmutableList()) collections?.forEach { - LoadAddressableNote(aTag = it, accountViewModel) { + LoadAddressableNote(it, accountViewModel) { it?.live()?.metadata?.observeAsState() } } 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 f157feeeee..7730ba8244 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 @@ -86,7 +86,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.SaveButton import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.ui.theme.placeholderText -import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedAddresses import com.vitorpamplona.quartz.nip30CustomEmoji.CustomEmoji import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag @@ -340,13 +340,7 @@ private fun EmojiSelector( onClick: ((EmojiUrlTag) -> Unit)? = null, ) { LoadAddressableNote( - aTag = - ATag( - EmojiPackSelectionEvent.KIND, - accountViewModel.userProfile().pubkeyHex, - "", - null, - ), + accountViewModel.account.getEmojiPackSelectionAddress(), accountViewModel, ) { emptyNote -> emptyNote?.let { usersEmojiList -> @@ -369,7 +363,7 @@ private fun EmojiSelector( @Composable fun EmojiCollectionGallery( - emojiCollections: ImmutableList, + emojiCollections: ImmutableList
, accountViewModel: AccountViewModel, nav: INav, onClick: ((EmojiUrlTag) -> Unit)? = null, @@ -382,8 +376,8 @@ fun EmojiCollectionGallery( LazyColumn( state = listState, ) { - itemsIndexed(emojiCollections, key = { _, item -> item.toTag() }) { _, item -> - LoadAddressableNote(aTag = item, accountViewModel) { + itemsIndexed(emojiCollections, key = { _, item -> item }) { _, item -> + LoadAddressableNote(item, accountViewModel) { it?.let { WatchAndRenderNote(it, bgColor, accountViewModel, nav, onClick) } } } 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 54af94aca0..5d734079ea 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 @@ -59,16 +59,13 @@ fun ShowForkInformation( val forkedAddress = remember(noteEvent) { noteEvent.forkFromAddress() } val forkedEvent = remember(noteEvent) { noteEvent.forkFromVersion() } if (forkedAddress != null) { - LoadAddressableNote( - aTag = forkedAddress, - accountViewModel = accountViewModel, - ) { addressableNote -> + LoadAddressableNote(forkedAddress, accountViewModel) { addressableNote -> if (addressableNote != null) { ForkInformationRowLightColor(addressableNote, modifier, accountViewModel, nav) } } } else if (forkedEvent != null) { - LoadNote(forkedEvent.eventId, accountViewModel = accountViewModel) { event -> + LoadNote(forkedEvent.eventId, accountViewModel) { event -> if (event != null) { ForkInformationRowLightColor(event, modifier, accountViewModel, nav) } 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 239ae46564..708e3df980 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 @@ -56,11 +56,9 @@ import com.vitorpamplona.amethyst.ui.note.elements.RemoveButton import com.vitorpamplona.amethyst.ui.note.getGradient 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.EmojiUrlTag import com.vitorpamplona.quartz.nip30CustomEmoji.pack.EmojiPackEvent -import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiPackSelectionEvent import com.vitorpamplona.quartz.nip30CustomEmoji.taggedEmojis @Composable @@ -180,13 +178,7 @@ private fun EmojiListOptions( emojiPackNote: Note, ) { LoadAddressableNote( - aTag = - ATag( - EmojiPackSelectionEvent.KIND, - accountViewModel.userProfile().pubkeyHex, - "", - null, - ), + accountViewModel.account.getEmojiPackSelectionAddress(), accountViewModel, ) { it?.let { usersEmojiList -> 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 03e3304ca5..17387b2236 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 @@ -137,10 +137,10 @@ private fun RenderGitPatchEvent( accountViewModel: AccountViewModel, nav: INav, ) { - val repository = remember(noteEvent) { noteEvent.repository() } + val repository = remember(noteEvent) { noteEvent.repositoryAddress() } if (repository != null) { - LoadAddressableNote(aTag = repository, accountViewModel = accountViewModel) { + LoadAddressableNote(repository, accountViewModel) { if (it != null) { RenderShortRepositoryHeader(it, accountViewModel, nav) Spacer(modifier = DoubleVertSpacer) @@ -242,10 +242,10 @@ private fun RenderGitIssueEvent( accountViewModel: AccountViewModel, nav: INav, ) { - val repository = remember(noteEvent) { noteEvent.repository() } + val repository = remember(noteEvent) { noteEvent.repositoryAddress() } if (repository != null) { - LoadAddressableNote(aTag = repository, accountViewModel = accountViewModel) { + LoadAddressableNote(repository, accountViewModel) { if (it != null) { RenderShortRepositoryHeader(it, accountViewModel, nav) Spacer(modifier = DoubleVertSpacer) 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 b0f0c9c963..bfbe434940 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 @@ -54,7 +54,7 @@ import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.navigation.routeFor import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip01Core.core.firstTagValueFor -import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip01Core.tags.events.ETag import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent @@ -81,7 +81,7 @@ fun RenderHighlight( context = noteEvent.context(), authorHex = noteEvent.pubKey, url = noteEvent.inUrl(), - postAddress = noteEvent.inPost(), + postAddress = noteEvent.inPostAddress(), postVersion = noteEvent.inPostVersion(), makeItShort = makeItShort, canPreview = canPreview, @@ -99,7 +99,7 @@ fun DisplayHighlight( context: String?, authorHex: String?, url: String?, - postAddress: ATag?, + postAddress: Address?, postVersion: ETag?, makeItShort: Boolean, canPreview: Boolean, @@ -149,7 +149,7 @@ private fun DisplayQuoteAuthor( highlightQuote: String, authorHex: String?, baseUrl: String?, - postAddress: ATag?, + postAddress: Address?, postVersion: ETag?, accountViewModel: AccountViewModel, nav: INav, @@ -165,7 +165,7 @@ private fun DisplayQuoteAuthor( } var addressable by remember { - mutableStateOf(postAddress?.let { accountViewModel.getAddressableNoteIfExists(it.toTag()) }) + mutableStateOf(postAddress?.let { accountViewModel.getAddressableNoteIfExists(it) }) } if (addressable == null && postAddress != null) { 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 5237d3a039..e7bc610267 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 @@ -66,13 +66,13 @@ fun RenderInteractiveStory( val rootEvent = note.value?.note?.event as? InteractiveStoryBaseEvent ?: return // keep updating the reading state event with new versions - val readingStateNote = accountViewModel.getInteractiveStoryReadingState(address.toTag()) + val readingStateNote = accountViewModel.getInteractiveStoryReadingState(address.toValue()) val latestReadingNoteState = readingStateNote.live().metadata.observeAsState() val readingState = latestReadingNoteState.value?.note?.event as? InteractiveStoryReadingStateEvent val currentScene = readingState?.currentScene() - if (currentScene != null && currentScene != rootEvent.aTag()) { + if (currentScene != null && currentScene != rootEvent.address()) { 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/RenderPostApproval.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RenderPostApproval.kt index b67dcf95ea..8b0b10280c 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 @@ -56,7 +56,7 @@ fun RenderPostApproval( val noteEvent = note.event as? CommunityPostApprovalEvent ?: return Column(Modifier.fillMaxWidth()) { - noteEvent.communities().forEach { tag -> + noteEvent.communityAddresses().forEach { tag -> LoadAddressableNote(tag, accountViewModel) { baseNote -> baseNote?.let { RenderCommunity( 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 c6bfd6c39d..c98795a6f2 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 @@ -127,7 +127,7 @@ class FollowListState( checkNotInMainThread() val hasNewList = - newNotes.any { + newNotes.any { it -> val noteEvent = it.event noteEvent?.pubKey == account.userProfile().pubkeyHex && 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 b9cbc1ef15..0c12b1f173 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 @@ -79,7 +79,7 @@ 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.addressables.Address import com.vitorpamplona.quartz.nip01Core.tags.people.PubKeyReferenceTag import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUserIds @@ -987,17 +987,17 @@ class AccountViewModel( } fun updateStatus( - it: ATag, + address: Address, newStatus: String, ) { viewModelScope.launch(Dispatchers.IO) { - account.updateStatus(LocalCache.getOrCreateAddressableNote(it), newStatus) + account.updateStatus(LocalCache.getOrCreateAddressableNote(address), newStatus) } } - fun deleteStatus(it: ATag) { + fun deleteStatus(address: Address) { viewModelScope.launch(Dispatchers.IO) { - account.deleteStatus(LocalCache.getOrCreateAddressableNote(it)) + account.deleteStatus(LocalCache.getOrCreateAddressableNote(address)) } } @@ -1124,10 +1124,10 @@ class AccountViewModel( viewModelScope.launch(Dispatchers.IO) { onResult(checkGetOrCreateAddressableNote(key)) } } - suspend fun getOrCreateAddressableNote(key: ATag): AddressableNote = LocalCache.getOrCreateAddressableNote(key) + suspend fun getOrCreateAddressableNote(key: Address): AddressableNote = LocalCache.getOrCreateAddressableNote(key) fun getOrCreateAddressableNote( - key: ATag, + key: Address, onResult: (AddressableNote?) -> Unit, ) { viewModelScope.launch(Dispatchers.IO) { onResult(getOrCreateAddressableNote(key)) } @@ -1135,6 +1135,8 @@ class AccountViewModel( fun getAddressableNoteIfExists(key: String): AddressableNote? = LocalCache.getAddressableNoteIfExists(key) + fun getAddressableNoteIfExists(key: Address): AddressableNote? = LocalCache.getAddressableNoteIfExists(key) + suspend fun findStatusesForUser( myUser: User, onResult: (ImmutableList) -> Unit, @@ -1539,7 +1541,7 @@ class AccountViewModel( ): Note { val note = if (innerEvent is AddressableEvent) { - AddressableNote(innerEvent.aTag()) + AddressableNote(innerEvent.address()) } else { Note(innerEvent.id) } @@ -1598,14 +1600,14 @@ class AccountViewModel( AdvertisedRelayListEvent.createAddressTag(user.pubkeyHex), ) - fun getInteractiveStoryReadingState(dATag: String): AddressableNote = LocalCache.getOrCreateAddressableNote(InteractiveStoryReadingStateEvent.createAddressATag(account.signer.pubKey, dATag)) + fun getInteractiveStoryReadingState(dATag: String): AddressableNote = LocalCache.getOrCreateAddressableNote(InteractiveStoryReadingStateEvent.createAddress(account.signer.pubKey, dATag)) fun updateInteractiveStoryReadingState( root: InteractiveStoryBaseEvent, readingScene: InteractiveStoryBaseEvent, ) { viewModelScope.launch(Dispatchers.IO) { - val sceneNoteRelayHint = LocalCache.getOrCreateAddressableNote(readingScene.aTag()).relayHintUrl() + val sceneNoteRelayHint = LocalCache.getOrCreateAddressableNote(readingScene.address()).relayHintUrl() val readingState = getInteractiveStoryReadingState(root.addressTag()) val readingStateEvent = readingState.event as? InteractiveStoryReadingStateEvent @@ -1613,7 +1615,7 @@ class AccountViewModel( if (readingStateEvent != null) { account.updateInteractiveStoryReadingState(readingStateEvent, readingScene, sceneNoteRelayHint) } else { - val rootNoteRelayHint = LocalCache.getOrCreateAddressableNote(root.aTag()).relayHintUrl() + val rootNoteRelayHint = LocalCache.getOrCreateAddressableNote(root.address()).relayHintUrl() account.createInteractiveStoryReadingState(root, rootNoteRelayHint, readingScene, sceneNoteRelayHint) } 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 a2d4e5ab65..38e95cb178 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 @@ -486,11 +486,7 @@ private suspend fun innerSendPost( imetas(usedAttachments) } } else { - if (channel.address.relay == null) { - channel.address.relay = channelRelays.firstOrNull() ?: replyingToEvent?.relay - } - - LiveActivitiesChatMessageEvent.message(tagger.message, channel.address) { + LiveActivitiesChatMessageEvent.message(tagger.message, channel.toATag()) { hashtags(findHashtags(tagger.message)) references(findURLs(tagger.message)) quotes(findNostrUris(tagger.message)) 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 2073699c13..4231abf3d7 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 @@ -176,7 +176,7 @@ import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.ZeroPadding import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.userProfileBorderModifier -import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedAddresses +import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedATags import com.vitorpamplona.quartz.nip01Core.tags.events.ETag import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEvents import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList @@ -704,7 +704,7 @@ private fun BookmarkTabHeader(baseUser: User) { ( bookmarkList?.taggedEvents()?.count() ?: 0 - ) + (bookmarkList?.taggedAddresses()?.count() ?: 0) + ) + (bookmarkList?.taggedATags()?.count() ?: 0) if (newBookmarks != userBookmarks) { userBookmarks = newBookmarks @@ -1349,8 +1349,8 @@ private fun DisplayBadges( nav: INav, ) { LoadAddressableNote( - aTag = BadgeProfilesEvent.createAddressTag(baseUser.pubkeyHex), - accountViewModel = accountViewModel, + BadgeProfilesEvent.createAddress(baseUser.pubkeyHex), + accountViewModel, ) { note -> if (note != null) { WatchAndRenderBadgeList( 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 c067946cdb..c16820c484 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 @@ -1004,7 +1004,7 @@ private fun RenderWikiHeaderForThread( } forkedAddress?.let { - LoadAddressableNote(aTag = it, accountViewModel = accountViewModel) { originalVersion -> + LoadAddressableNote(it, accountViewModel) { originalVersion -> if (originalVersion != null) { ForkInformationRow(originalVersion, Modifier.fillMaxWidth(), accountViewModel, nav) } 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 822a634aa5..2847cb4bc5 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/blossom/BlossomServersEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/blossom/BlossomServersEvent.kt @@ -25,6 +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.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.utils.TimeUtils @@ -50,6 +51,8 @@ class BlossomServersEvent( const val KIND = 10063 const val ALT = "File servers used by the author" + fun createAddress(pubKey: HexKey): Address = Address(KIND, pubKey, FIXED_D_TAG) + fun createAddressATag(pubKey: HexKey): ATag = ATag(KIND, pubKey, FIXED_D_TAG, null) fun createAddressTag(pubKey: HexKey): String = ATag.assembleATagId(KIND, pubKey, FIXED_D_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 74b069dc47..80340b4073 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 @@ -29,6 +29,7 @@ import com.vitorpamplona.quartz.nip01Core.core.TagArray 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.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip21UriScheme.toNostrUri import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.utils.TimeUtils @@ -102,6 +103,8 @@ class PrivateOutboxRelayListEvent( const val KIND = 10013 val TAGS = arrayOf(AltTag.assemble("Relay list to store private content from this author")) + fun createAddress(pubKey: HexKey): Address = Address(KIND, pubKey, FIXED_D_TAG) + fun createAddressATag(pubKey: HexKey): ATag = ATag(KIND, pubKey, FIXED_D_TAG, null) fun createAddressTag(pubKey: HexKey): String = ATag.assembleATagId(KIND, pubKey, FIXED_D_TAG) 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 index 45c97f641e..af5f4e101f 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/forks/BaseThreadedEventExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/forks/BaseThreadedEventExt.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.quartz.experimental.forks 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.nip10Notes.BaseThreadedEvent import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag @@ -30,9 +30,7 @@ fun BaseThreadedEvent.isAFork() = tags.any { it.size > 3 && (it[0] == "a" || it[ 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) + Address.parse(aTagValue) } fun BaseThreadedEvent.forkFromVersion() = tags.firstNotNullOfOrNull(MarkedETag::parseFork) 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 c81c9ab4ea..1ee7fab68c 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 @@ -30,6 +30,7 @@ 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.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag import com.vitorpamplona.quartz.nip23LongContent.tags.ImageTag import com.vitorpamplona.quartz.nip23LongContent.tags.TitleTag @@ -57,13 +58,18 @@ class InteractiveStoryReadingStateEvent( fun root() = tags.firstNotNullOfOrNull(RootSceneTag::parse) - fun currentScene() = tags.firstNotNullOfOrNull(ATag::parse) + fun currentScene() = tags.firstNotNullOfOrNull(ATag::parseAddress) companion object { const val KIND = 30298 const val ALT1 = "Interactive Story Reading state" const val ALT2 = "The reading state of " + fun createAddress( + pubKey: HexKey, + dtag: String, + ): Address = Address(KIND, pubKey, dtag) + fun createAddressATag( pubKey: HexKey, dtag: String, 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 index b4a5fabc93..6327c05270 100644 --- 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 @@ -20,14 +20,16 @@ */ package com.vitorpamplona.quartz.experimental.interactiveStories.tags -import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address +import com.vitorpamplona.quartz.nip46RemoteSigner.getOrNull import com.vitorpamplona.quartz.utils.arrayOfNotNull class StoryOptionTag( val option: String, - val address: ATag, + val address: Address, + val relay: String?, ) { - fun toTagArray() = assemble(option, address) + fun toTagArray() = assemble(option, address, relay) companion object { const val TAG_NAME = "option" @@ -37,13 +39,16 @@ class StoryOptionTag( 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) } + val address = Address.parse(tag[2]) ?: return null + + return StoryOptionTag(tag[1], address, tag.getOrNull(3)) } @JvmStatic fun assemble( title: String, - destination: ATag, - ) = arrayOfNotNull(TAG_NAME, title, destination.toTag(), destination.relay) + address: Address, + relay: String?, + ) = arrayOfNotNull(TAG_NAME, title, address.toValue(), relay) } } 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 04d0a2d45c..16019c7269 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 @@ -46,5 +46,5 @@ open class BaseAddressableEvent( /** * Creates the tag in a memory efficient way (without creating the ATag class */ - override fun addressTag() = ATag.assembleATagId(kind, pubKey, dTag()) + override fun addressTag() = Address.assemble(kind, pubKey, dTag()) } 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 9535c08bb9..184fe4eea7 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 @@ -44,7 +44,7 @@ open class BaseReplaceableEvent( /** * Creates the tag in a memory efficient way (without creating the ATag class */ - override fun addressTag() = ATag.assembleATagId(kind, pubKey, FIXED_D_TAG) + override fun addressTag() = Address.assemble(kind, pubKey, dTag()) companion object { const val FIXED_D_TAG = "" 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 a6decad3f5..10c9e67505 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 @@ -34,19 +34,9 @@ data class ATag( val kind: Int, val pubKeyHex: String, val dTag: String, + val relay: String? = null, ) { - var relay: String? = null - - constructor(address: Address) : this(address.kind, address.pubKeyHex, address.dTag) - - constructor( - kind: Int, - pubKeyHex: HexKey, - dTag: String, - relayHint: String?, - ) : this(kind, pubKeyHex, dTag) { - this.relay = relayHint - } + constructor(address: Address, relayHint: String? = null) : this(address.kind, address.pubKeyHex, address.dTag, relayHint) fun countMemory(): Long = 5 * pointerSizeInBytes + // 7 fields, 4 bytes each reference (32bit) @@ -129,6 +119,9 @@ data class ATag( @JvmStatic fun parseAddress(tag: Array) = ATagParser.parseAddress(TAG_NAME, tag) + @JvmStatic + fun parseAddressId(tag: Array) = ATagParser.parseAddressId(TAG_NAME, tag) + @JvmStatic fun assemble( aTagId: HexKey, 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 index 9d8c64ffb6..2523600078 100644 --- 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 @@ -99,6 +99,15 @@ class ATagParser { fun parseAddress( tagName: String, tag: Array, + ): Address? { + if (tag.isNotName(tagName, TAG_SIZE)) return null + return Address.parse(tag[1]) + } + + @JvmStatic + fun parseAddressId( + tagName: String, + tag: Array, ): String? { if (tag.isNotName(tagName, TAG_SIZE)) return null return tag[1] 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 index 0591f2ac37..aa9ffe4da0 100644 --- 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 @@ -26,7 +26,7 @@ import com.vitorpamplona.quartz.utils.Hex import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.pointerSizeInBytes -class Address( +data class Address( val kind: Int, val pubKeyHex: HexKey, val dTag: String, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/EventExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/EventExt.kt index d0ba9582ed..1759536487 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/EventExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/EventExt.kt @@ -34,6 +34,10 @@ fun Event.isTaggedAddressableKind(kind: Int) = tags.isTaggedAddressableKind(kind fun Event.getTagOfAddressableKind(kind: Int) = tags.getTagOfAddressableKind(kind) +fun Event.taggedATags() = tags.taggedATags() + +fun Event.firstTaggedATag() = tags.firstTaggedATag() + fun Event.taggedAddresses() = tags.taggedAddresses() fun Event.firstTaggedAddress() = tags.firstTaggedAddress() 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 fe2fd46f01..d7e8deb0e8 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 @@ -29,14 +29,18 @@ fun TagArray.mapTaggedAddress(map: (address: String) -> R) = this.mapValueTa fun TagArray.firstIsTaggedAddressableNote(addressableNotes: Set) = this.firstNotNullOfOrNull(ATag::parseIfIsIn, addressableNotes) -fun TagArray.isTaggedAddressableNote(idHex: String) = this.any(ATag::isTagged, idHex) +fun TagArray.isTaggedAddressableNote(addressId: String) = this.any(ATag::isTagged, addressId) -fun TagArray.isTaggedAddressableNotes(idHexes: Set) = this.any(ATag::isIn, idHexes) +fun TagArray.isTaggedAddressableNotes(addressIds: Set) = this.any(ATag::isIn, addressIds) fun TagArray.isTaggedAddressableKind(kind: Int) = this.any(ATag::isTaggedWithKind, kind.toString()) fun TagArray.getTagOfAddressableKind(kind: Int) = this.firstNotNullOfOrNull(ATag::parseIfOfKind, kind.toString()) -fun TagArray.taggedAddresses() = this.mapNotNull(ATag::parse) +fun TagArray.taggedATags() = this.mapNotNull(ATag::parse) -fun TagArray.firstTaggedAddress() = this.firstNotNullOfOrNull(ATag::parse) +fun TagArray.firstTaggedATag() = this.firstNotNullOfOrNull(ATag::parse) + +fun TagArray.taggedAddresses() = this.mapNotNull(ATag::parseAddress) + +fun TagArray.firstTaggedAddress() = this.firstNotNullOfOrNull(ATag::parseAddress) 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 7eeb1be6d0..0714508b59 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip09Deletions/DeletionEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip09Deletions/DeletionEvent.kt @@ -55,7 +55,7 @@ class DeletionEvent( fun deleteAddresses() = taggedAddresses() - fun deleteAddressTags() = tags.mapNotNull(ATag::parseAddress) + fun deleteAddressIds() = tags.mapNotNull(ATag::parseAddressId) companion object { const val KIND = 5 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/BaseThreadedEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/BaseThreadedEvent.kt index 310d67085a..bef9ca4b41 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/BaseThreadedEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/BaseThreadedEvent.kt @@ -23,7 +23,7 @@ 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.taggedAddresses +import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedATags import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUsers import com.vitorpamplona.quartz.nip10Notes.content.findIndexTagsWithEventsOrAddresses import com.vitorpamplona.quartz.nip10Notes.content.findIndexTagsWithPeople @@ -141,7 +141,7 @@ open class BaseThreadedEvent( val uncertainRepliesTo = unmarkedReplyTos() val tagAddresses = - taggedAddresses() + taggedATags() .filter { it.kind != CommunityDefinitionEvent.KIND && (kind != WikiNoteEvent.KIND || it.kind != WikiNoteEvent.KIND) // removes forks from itself. diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/settings/ChatMessageRelayListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/settings/ChatMessageRelayListEvent.kt index b6177171ac..c53ccaca25 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/settings/ChatMessageRelayListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/settings/ChatMessageRelayListEvent.kt @@ -26,6 +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.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.utils.TimeUtils @@ -50,6 +51,8 @@ class ChatMessageRelayListEvent( companion object { const val KIND = 10050 + fun createAddress(pubKey: HexKey): Address = Address(KIND, pubKey, FIXED_D_TAG) + fun createAddressATag(pubKey: HexKey): ATag = ATag(KIND, pubKey, FIXED_D_TAG, null) fun createAddressTag(pubKey: HexKey): String = ATag.assembleATagId(KIND, pubKey, FIXED_D_TAG) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/entities/NAddress.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/entities/NAddress.kt index ed507b0037..83720e5b28 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/entities/NAddress.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/entities/NAddress.kt @@ -75,12 +75,14 @@ data class NAddress( kind: Int, pubKeyHex: String, dTag: String, - relay: String?, + vararg relays: String?, ): String = TlvBuilder() .apply { addString(TlvTypes.SPECIAL, dTag) - addStringIfNotNull(TlvTypes.RELAY, relay) + relays.forEach { + addStringIfNotNull(TlvTypes.RELAY, it) + } addHex(TlvTypes.AUTHOR, pubKeyHex) addInt(TlvTypes.KIND, kind) }.build() diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/entities/NEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/entities/NEvent.kt index bf36b99788..7d7d1ae2e1 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/entities/NEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/entities/NEvent.kt @@ -59,12 +59,14 @@ data class NEvent( idHex: String, author: String?, kind: Int?, - relay: String?, + vararg relays: String?, ): String = TlvBuilder() .apply { addHex(TlvTypes.SPECIAL, idHex) - addStringIfNotNull(TlvTypes.RELAY, relay) + relays.forEach { + addStringIfNotNull(TlvTypes.RELAY, it) + } addHexIfNotNull(TlvTypes.AUTHOR, author) addIntIfNotNull(TlvTypes.KIND, kind) }.build() 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 fe75d90341..1729b0d843 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip23LongContent/LongTextNoteEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip23LongContent/LongTextNoteEvent.kt @@ -54,7 +54,7 @@ class LongTextNoteEvent( override fun address() = Address(kind, pubKey, dTag()) - override fun addressTag() = ATag.assembleATagId(kind, pubKey, dTag()) + override fun addressTag() = Address.assemble(kind, pubKey, dTag()) fun topics() = hashtags() 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 index aeacfc2c2f..b55cb6eb8e 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip30CustomEmoji/selection/EmojiPackSelectionEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip30CustomEmoji/selection/EmojiPackSelectionEvent.kt @@ -28,7 +28,8 @@ 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.nip01Core.tags.addressables.Address +import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedATags import com.vitorpamplona.quartz.nip30CustomEmoji.pack.EmojiPackEvent import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.utils.TimeUtils @@ -42,13 +43,14 @@ class EmojiPackSelectionEvent( content: String, sig: HexKey, ) : BaseReplaceableEvent(id, pubKey, createdAt, KIND, tags, content, sig) { - fun emojiPacks() = taggedAddresses() + fun emojiPacks() = taggedATags() companion object { const val KIND = 10030 const val ALT_DESCRIPTION = "Emoji selection" + const val FIXED_D_TAG = "" - fun createAddressATag(pubKey: HexKey) = ATag(KIND, pubKey, FIXED_D_TAG, null) + fun createAddress(pubKey: HexKey) = Address(KIND, pubKey, FIXED_D_TAG) fun createAddressTag(pubKey: HexKey) = ATag.assembleATagId(KIND, pubKey, FIXED_D_TAG) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/issue/GitIssueEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/issue/GitIssueEvent.kt index 8f441aebd1..2ca47f1382 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/issue/GitIssueEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/issue/GitIssueEvent.kt @@ -43,7 +43,9 @@ class GitIssueEvent( content: String, sig: HexKey, ) : BaseThreadedEvent(id, pubKey, createdAt, KIND, tags, content, sig) { - fun repositoryHex() = tags.firstNotNullOfOrNull(ATag::parseAddress) + fun repositoryHex() = tags.firstNotNullOfOrNull(ATag::parseAddressId) + + fun repositoryAddress() = tags.firstNotNullOfOrNull(ATag::parseAddress) fun repository() = tags.firstNotNullOfOrNull(ATag::parse) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/patch/GitPatchEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/patch/GitPatchEvent.kt index 5f7a6cbf3b..f7bfa67500 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/patch/GitPatchEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/patch/GitPatchEvent.kt @@ -25,6 +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.addressables.ATag +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.utils.TimeUtils @@ -43,6 +44,15 @@ class GitPatchEvent( private fun repositoryHex() = innerRepository()?.getOrNull(1) + fun repositoryAddress() = + innerRepository()?.let { + if (it.size > 1) { + Address.parse(it[1]) + } else { + null + } + } + fun repository() = innerRepository()?.let { if (it.size > 1) { 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 index d8f6e1ba10..538a4e51fd 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/reply/GitReplyEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/reply/GitReplyEvent.kt @@ -44,7 +44,7 @@ class GitReplyEvent( content: String, sig: HexKey, ) : BaseThreadedEvent(id, pubKey, createdAt, KIND, tags, content, sig) { - fun repositoryHex() = tags.firstNotNullOfOrNull(ATag::parseAddress) + fun repositoryHex() = tags.firstNotNullOfOrNull(ATag::parseAddressId) fun repository() = tags.firstNotNullOfOrNull(ATag::parse) 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 1e17ada6ab..5f78cb5a08 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip50Search/SearchRelayListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip50Search/SearchRelayListEvent.kt @@ -26,6 +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.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.utils.TimeUtils @@ -50,6 +51,8 @@ class SearchRelayListEvent( companion object { const val KIND = 10007 + fun createAddress(pubKey: HexKey): Address = Address(KIND, pubKey, FIXED_D_TAG) + fun createAddressATag(pubKey: HexKey): ATag = ATag(KIND, pubKey, FIXED_D_TAG, null) fun createAddressTag(pubKey: HexKey): String = ATag.assembleATagId(KIND, pubKey, FIXED_D_TAG) 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 07920511cd..ef8aec468a 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/GeneralListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/GeneralListEvent.kt @@ -26,6 +26,7 @@ import com.vitorpamplona.quartz.nip01Core.core.isTagged 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.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEvents import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohashes import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUserIds @@ -113,9 +114,14 @@ abstract class GeneralListEvent( onReady: (List) -> Unit, ) = privateTags(signer) { onReady(filterEvents(it)) } - fun privateTaggedAddresses( + fun privateATags( signer: NostrSigner, onReady: (List) -> Unit, + ) = privateTags(signer) { onReady(filterATags(it)) } + + fun privateAddress( + signer: NostrSigner, + onReady: (List
) -> Unit, ) = privateTags(signer) { onReady(filterAddresses(it)) } fun filterUsers(tags: Array>): List = tags.filter { it.size > 1 && it[0] == "p" }.map { it[1] } @@ -126,15 +132,9 @@ abstract class GeneralListEvent( fun filterEvents(tags: Array>): List = tags.filter { it.size > 1 && it[0] == "e" }.map { it[1] } - fun filterAddresses(tags: Array>): List = - tags - .filter { it.firstOrNull() == "a" } - .mapNotNull { - val aTagValue = it.getOrNull(1) - val relay = it.getOrNull(2) + fun filterATags(tags: Array>): List = tags.mapNotNull(ATag::parse) - if (aTagValue != null) ATag.parse(aTagValue, relay) else null - } + fun filterAddresses(tags: Array>): List
= tags.mapNotNull(ATag::parseAddress) companion object { fun createPrivateTags( 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 23d91f8899..8c1c589f8e 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/MuteListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/MuteListEvent.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.quartz.nip51Lists 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.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.bytesUsedInMemory @@ -77,8 +77,11 @@ class MuteListEvent( companion object { const val KIND = 10000 + const val FIXED_D_TAG = "" const val ALT = "Mute List" + fun createAddress(pubKey: HexKey) = Address(KIND, pubKey, FIXED_D_TAG) + fun blockListFor(pubKeyHex: HexKey): String = "10000:$pubKeyHex:" fun createListWithTag( 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 8098258531..8b03f1ec7e 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/PeopleListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/PeopleListEvent.kt @@ -23,6 +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.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.bytesUsedInMemory @@ -127,6 +128,8 @@ class PeopleListEvent( const val BLOCK_LIST_D_TAG = "mute" const val ALT = "List of people" + fun createBlockAddress(pubKey: HexKey) = Address(KIND, pubKey, BLOCK_LIST_D_TAG) + fun blockListFor(pubKeyHex: HexKey): String = "30000:$pubKeyHex:$BLOCK_LIST_D_TAG" fun createListWithTag( 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 index c1ab8889d0..ebf5926dc4 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/chat/LiveActivitiesChatMessageEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/chat/LiveActivitiesChatMessageEvent.kt @@ -39,10 +39,12 @@ class LiveActivitiesChatMessageEvent( content: String, sig: HexKey, ) : BaseThreadedEvent(id, pubKey, createdAt, KIND, tags, content, sig) { - private fun activityHex() = tags.firstNotNullOfOrNull(ATag::parseAddress) + private fun activityHex() = tags.firstNotNullOfOrNull(ATag::parseAddressId) fun activity() = tags.firstNotNullOfOrNull(ATag::parse) + fun activityAddress() = tags.firstNotNullOfOrNull(ATag::parseAddress) + override fun markedReplyTos() = super.markedReplyTos().minus(activityHex() ?: "") override fun unmarkedReplyTos() = super.markedReplyTos().minus(activityHex() ?: "") 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 705a1501eb..2b3b27b457 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip54Wiki/WikiNoteEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip54Wiki/WikiNoteEvent.kt @@ -48,7 +48,7 @@ class WikiNoteEvent( override fun address() = Address(kind, pubKey, dTag()) - override fun addressTag() = ATag.assembleATagId(kind, pubKey, dTag()) + override fun addressTag() = Address.assemble(kind, pubKey, dTag()) fun topics() = hashtags() diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip58Badges/BadgeProfilesEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip58Badges/BadgeProfilesEvent.kt index a312c559e1..8493516a53 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip58Badges/BadgeProfilesEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip58Badges/BadgeProfilesEvent.kt @@ -24,6 +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.tags.addressables.ATag +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedAddresses import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEvents @@ -45,6 +46,8 @@ class BadgeProfilesEvent( private const val STANDARD_D_TAG = "profile_badges" private const val ALT = "List of accepted badges by the author" + fun createAddress(pubKey: HexKey): Address = Address(KIND, pubKey, STANDARD_D_TAG) + fun createAddressTag(pubKey: HexKey): ATag = ATag(KIND, pubKey, STANDARD_D_TAG, null) } } 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 3cd3bc662f..e0b918f447 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip65RelayList/AdvertisedRelayListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip65RelayList/AdvertisedRelayListEvent.kt @@ -26,6 +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.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.utils.TimeUtils @@ -85,6 +86,8 @@ class AdvertisedRelayListEvent( const val KIND = 10002 const val ALT = "Relay list to discover the user's content" + fun createAddress(pubKey: HexKey): Address = Address(KIND, pubKey, FIXED_D_TAG) + fun createAddressATag(pubKey: HexKey): ATag = ATag(KIND, pubKey, FIXED_D_TAG, null) fun createAddressTag(pubKey: HexKey): String = ATag.assembleATagId(KIND, pubKey, FIXED_D_TAG) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/approval/CommunityPostApprovalEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/approval/CommunityPostApprovalEvent.kt index 3ff83eaba3..049843e0bb 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/approval/CommunityPostApprovalEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/approval/CommunityPostApprovalEvent.kt @@ -27,6 +27,7 @@ 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.nip01Core.tags.addressables.taggedATags import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedAddresses import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEvents import com.vitorpamplona.quartz.nip01Core.tags.kinds.kind @@ -54,10 +55,14 @@ class CommunityPostApprovalEvent( null } - fun communities() = taggedAddresses().filter { it.kind == CommunityDefinitionEvent.KIND } + fun communities() = taggedATags().filter { it.kind == CommunityDefinitionEvent.KIND } + + fun communityAddresses() = taggedAddresses().filter { it.kind == CommunityDefinitionEvent.KIND } fun approvedEvents() = taggedEvents() + fun approvedATags() = taggedATags().filter { it.kind != CommunityDefinitionEvent.KIND } + fun approvedAddresses() = taggedAddresses().filter { it.kind != CommunityDefinitionEvent.KIND } companion object { 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 b030fb84e5..e02f6e034f 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip78AppData/AppSpecificDataEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip78AppData/AppSpecificDataEvent.kt @@ -24,6 +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.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.utils.TimeUtils @@ -39,6 +40,11 @@ class AppSpecificDataEvent( const val KIND = 30078 const val ALT = "Arbitrary app data" + fun createAddress( + pubKey: HexKey, + dTag: String, + ) = Address(KIND, pubKey, dTag) + fun createTag( pubkey: HexKey, dTag: String, 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 6b50b3c813..9f8bcc63c2 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip84Highlights/HighlightEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip84Highlights/HighlightEvent.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.quartz.nip84Highlights 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.firstTaggedATag import com.vitorpamplona.quartz.nip01Core.tags.addressables.firstTaggedAddress import com.vitorpamplona.quartz.nip01Core.tags.events.firstTaggedEvent import com.vitorpamplona.quartz.nip01Core.tags.people.firstTaggedUser @@ -49,7 +50,9 @@ class HighlightEvent( fun context() = tags.firstNotNullOfOrNull(ContextTag::parse) - fun inPost() = firstTaggedAddress() + fun inPost() = firstTaggedATag() + + fun inPostAddress() = firstTaggedAddress() fun inPostVersion() = firstTaggedEvent() diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip96FileStorage/config/FileServersEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip96FileStorage/config/FileServersEvent.kt index 0724181706..ffaefe6590 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip96FileStorage/config/FileServersEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip96FileStorage/config/FileServersEvent.kt @@ -26,6 +26,7 @@ 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.tags.addressables.ATag +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.nip96FileStorage.config.tags.ServerTag import com.vitorpamplona.quartz.utils.TimeUtils @@ -45,6 +46,8 @@ class FileServersEvent( const val KIND = 10096 const val ALT_DESCRIPTOR = "File servers used by the author" + fun createAddress(pubKey: HexKey): Address = Address(KIND, pubKey, FIXED_D_TAG) + fun createAddressATag(pubKey: HexKey): ATag = ATag(KIND, pubKey, FIXED_D_TAG, null) fun createAddressTag(pubKey: HexKey): String = ATag.assembleATagId(KIND, pubKey, FIXED_D_TAG) From 50da982b363b2360aba0b4076f53c4b09486f6b4 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Mon, 17 Feb 2025 19:14:58 +0000 Subject: [PATCH 24/76] New Crowdin translations by GitHub Action --- amethyst/src/main/res/values-fa/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/amethyst/src/main/res/values-fa/strings.xml b/amethyst/src/main/res/values-fa/strings.xml index cc1f5b90d4..71a721d723 100644 --- a/amethyst/src/main/res/values-fa/strings.xml +++ b/amethyst/src/main/res/values-fa/strings.xml @@ -909,7 +909,7 @@ خدمت در دسترس نیست - این اغلب هنگامی که یک سرور بیش از حد شلوغ است یا برای تعمیرات تعطیل است پیش می آید مهلت درگاه تمام شد - مهلت سرور در نقش یک درگاه یا پراکسی تمام شد، در انتظار پاسخ نسخه HTTP پشتیبانی نمی شود - سرور ورژن HTTP درخواست را پشتیبانی نمی کند - گونه نیز مذاکره - سرور یک مشکل پیکربندی داخلی دارد + Variant Also Negotiates - سرور یک مشکل پیکربندی داخلی دارد فضای ذخیره ناکافی - سرور فضای کافی برای پردازش موفق درخواست ندارد حلقه دیده شد - سرور یک حلقه بینهایت در پردازش این درخواست شناسایی کرد احراز هویت الزامی - کلاینت برای دسترسی به شبکه می بایست احراز هویت شده باشد From 6d9964a1cb63f624c5734eed11e30fdecdb3eb87 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 17 Feb 2025 19:13:05 -0500 Subject: [PATCH 25/76] Decoupling NIP01 methods from CryptoUtils Decoupling Encryption and Decryptions from CryptoUtils Decoupling live instances of JNI bindings for Secp and LibSodium from CryptoUtils Decoupling key cache from CryptoUtils Reorganizes NIP-04 to match new package structure Adjusts test structures to match --- .../amethyst/ImageUploadTesting.kt | 6 +- .../vitorpamplona/amethyst/model/Account.kt | 4 +- .../vitorpamplona/amethyst/model/Chatroom.kt | 2 +- .../amethyst/model/LocalCache.kt | 2 +- .../amethyst/service/Base64Image.kt | 4 +- .../service/NostrChatroomDataSource.kt | 2 +- .../service/NostrChatroomListDataSource.kt | 2 +- .../NostrSearchEventOrUserDataSource.kt | 6 +- .../amethyst/service/OnlineCheck.kt | 4 +- .../EventNotificationConsumer.kt | 2 +- .../amethyst/service/uploads/EncryptFiles.kt | 6 +- .../amethyst/service/uploads/FileHeader.kt | 4 +- .../uploads/blossom/BlossomUploader.kt | 4 +- .../vitorpamplona/amethyst/ui/MainActivity.kt | 2 +- .../amethyst/ui/actions/NewMessageTagger.kt | 4 +- .../amethyst/ui/actions/NewPostViewModel.kt | 2 +- .../amethyst/ui/components/ClickableRoute.kt | 2 +- .../ui/components/ZoomableContentView.kt | 4 +- .../ui/dal/ChatroomListNewFeedFilter.kt | 2 +- .../amethyst/ui/dal/GeoHashFeedFilter.kt | 2 +- .../amethyst/ui/dal/HashtagFeedFilter.kt | 2 +- .../amethyst/ui/note/NoteCompose.kt | 2 +- .../amethyst/ui/note/types/PrivateMessage.kt | 2 +- .../ui/screen/AccountStateViewModel.kt | 13 +- .../ui/screen/loggedIn/AccountBackupDialog.kt | 6 +- .../ui/screen/loggedIn/AccountViewModel.kt | 4 +- .../chatrooms/ChatroomMessageCompose.kt | 2 +- .../notifications/CardFeedContentState.kt | 2 +- .../loggedIn/threadview/ThreadFeedView.kt | 2 +- .../quartz/benchmark/EventBenchmark.kt | 11 +- .../quartz/benchmark/GiftWrapBenchmark.kt | 2 - .../benchmark/GiftWrapReceivingBenchmark.kt | 10 +- .../quartz/benchmark/Sha256Benchmark.kt | 4 +- ...yptoBenchmark.kt => SharedKeyBenchmark.kt} | 48 +---- .../quartz/benchmark/SignVerifyBenchmark.kt | 62 ++++++ .../commons/robohash/RobohashAssembler.kt | 4 +- .../vitorpamplona/quartz/CryptoUtilsTest.kt | 140 ------------- .../vitorpamplona/quartz/bloom/BloomFilter.kt | 7 +- .../quartz/nip01Core/Nip01Test.kt | 32 ++- .../nip01Core/crypto/Nip01CryptoTest.kt | 39 ++++ ...p04EncryptionTest.kt => EncryptionTest.kt} | 22 +- .../vitorpamplona/quartz/nip04Dm/Nip04Test.kt | 57 ++++++ .../Bip32SeedDerivationTest.kt | 3 +- .../nip06KeyDerivation/Bip39MnemonicsTest.kt | 4 +- .../quartz/nip06KeyDerivation/Nip06Test.kt | 3 +- .../quartz/nip44Encryption/Nip44v1Test.kt | 94 +++++++++ .../{NIP44v2Test.kt => Nip44v2Test.kt} | 24 +-- .../quartz/nip49PrivKeyEnc/NIP49Test.kt | 5 +- .../nip55AndroidSigner/SignStringTest.kt | 49 +++++ .../quartz/utils/HexEncodingTest.kt | 7 +- .../com/vitorpamplona/quartz/CryptoUtils.kt | 189 ------------------ .../com/vitorpamplona/quartz/EventFactory.kt | 2 +- .../quartz/nip01Core/EventExt.kt | 4 +- .../quartz/nip01Core/EventHasher.kt | 4 +- .../quartz/nip01Core/crypto/KeyPair.kt | 13 +- .../quartz/nip01Core/crypto/Nip01.kt | 38 +--- .../quartz/nip01Core/signers/NostrSigner.kt | 4 +- .../nip01Core/signers/NostrSignerSync.kt | 28 ++- .../quartz/nip04Dm/Nip04Encryption.kt | 130 ------------ .../quartz/nip04Dm/crypto/AESCBC.kt | 56 ++++++ .../EncryptedInfo.kt} | 39 ++-- .../quartz/nip04Dm/crypto/Encryption.kt | 76 +++++++ .../quartz/nip04Dm/crypto/Nip04.kt | 63 ++++++ .../nip04Dm/{ => messages}/PrivateDmEvent.kt | 2 +- .../{ => messages}/TagArrayBuilderExt.kt | 2 +- .../nip06KeyDerivation/Bip32SeedDerivation.kt | 35 +--- .../nip06KeyDerivation/Bip39Mnemonics.kt | 6 +- .../quartz/nip06KeyDerivation/Nip06.kt | 8 +- .../quartz/nip17Dm/files/encryption/AESGCM.kt | 6 +- .../quartz/nip19Bech32/Nip19Parser.kt | 6 +- .../EncryptedInfoString.kt} | 11 +- .../quartz/nip44Encryption/Nip44.kt | 46 +---- .../quartz/nip44Encryption/Nip44v1.kt | 42 ++-- .../quartz/nip44Encryption/Nip44v2.kt | 53 ++--- .../nip44Encryption/crypto/SodiumUtils.kt | 103 ---------- .../quartz/nip49PrivKeyEnc/ByteArrayExt.kt | 25 +++ .../quartz/nip49PrivKeyEnc/Nip49.kt | 43 +--- .../nip55AndroidSigner/NostrSignerExternal.kt | 17 -- .../quartz/nip55AndroidSigner/SignString.kt | 31 +++ .../quartz/nip57Zaps/PrivateZapEncryption.kt | 9 +- .../nip57Zaps/PrivateZapRequestBuilder.kt | 4 +- .../nip98HttpAuth/tags/PayloadHashTag.kt | 4 +- .../vitorpamplona/quartz/utils/ArrayUtils.kt | 2 - .../com/vitorpamplona/quartz/utils/Hmac512.kt | 33 +++ .../quartz/utils/LibSodiumInstance.kt | 105 ++++++++++ .../quartz/utils/RandomInstance.kt | 31 +++ .../quartz/utils/Secp256k1Instance.kt | 59 ++++++ .../com/vitorpamplona/quartz/utils/Sha256.kt | 2 +- .../vitorpamplona/quartz/utils/TimeUtils.kt | 4 +- 89 files changed, 1048 insertions(+), 1015 deletions(-) rename benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/{CryptoBenchmark.kt => SharedKeyBenchmark.kt} (63%) create mode 100644 benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/SignVerifyBenchmark.kt delete mode 100644 quartz/src/androidTest/java/com/vitorpamplona/quartz/CryptoUtilsTest.kt create mode 100644 quartz/src/androidTest/java/com/vitorpamplona/quartz/nip01Core/crypto/Nip01CryptoTest.kt rename quartz/src/androidTest/java/com/vitorpamplona/quartz/nip04Dm/{Nip04EncryptionTest.kt => EncryptionTest.kt} (77%) create mode 100644 quartz/src/androidTest/java/com/vitorpamplona/quartz/nip04Dm/Nip04Test.kt create mode 100644 quartz/src/androidTest/java/com/vitorpamplona/quartz/nip44Encryption/Nip44v1Test.kt rename quartz/src/androidTest/java/com/vitorpamplona/quartz/nip44Encryption/{NIP44v2Test.kt => Nip44v2Test.kt} (90%) create mode 100644 quartz/src/androidTest/java/com/vitorpamplona/quartz/nip55AndroidSigner/SignStringTest.kt delete mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/CryptoUtils.kt delete mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/Nip04Encryption.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/crypto/AESCBC.kt rename quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/{Nip04Encoder.kt => crypto/EncryptedInfo.kt} (70%) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/crypto/Encryption.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/crypto/Nip04.kt rename quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/{ => messages}/PrivateDmEvent.kt (99%) rename quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/{ => messages}/TagArrayBuilderExt.kt (97%) rename quartz/src/main/java/com/vitorpamplona/quartz/{utils/RandomExt.kt => nip44Encryption/EncryptedInfoString.kt} (87%) delete mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip44Encryption/crypto/SodiumUtils.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip49PrivKeyEnc/ByteArrayExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/SignString.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/utils/Hmac512.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/utils/LibSodiumInstance.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/utils/RandomInstance.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/utils/Secp256k1Instance.kt diff --git a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ImageUploadTesting.kt b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ImageUploadTesting.kt index e8aaca5677..2b1c8becbc 100644 --- a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ImageUploadTesting.kt +++ b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ImageUploadTesting.kt @@ -34,9 +34,9 @@ import com.vitorpamplona.amethyst.service.uploads.nip96.ServerInfoRetriever 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.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.toHexKey +import com.vitorpamplona.quartz.utils.sha256 import junit.framework.TestCase.assertEquals import junit.framework.TestCase.fail import kotlinx.coroutines.CoroutineScope @@ -81,7 +81,7 @@ class ImageUploadTesting { private suspend fun testBlossom(server: ServerName) { val paylod = getBitmap() - val initialHash = CryptoUtils.sha256(paylod).toHexKey() + val initialHash = sha256(paylod).toHexKey() val inputStream = paylod.inputStream() val result = BlossomUploader() @@ -111,7 +111,7 @@ class ImageUploadTesting { return } - val downloadedHash = CryptoUtils.sha256(imageData).toHexKey() + val downloadedHash = sha256(imageData).toHexKey() assertEquals(initialHash, downloadedHash) } 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 5907b79d11..712ff187f8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -102,8 +102,8 @@ 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.nip04Dm.messages.PrivateDmEvent +import com.vitorpamplona.quartz.nip04Dm.messages.reply import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip10Notes.content.findHashtags diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Chatroom.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Chatroom.kt index dc4e636489..dbf77b216f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Chatroom.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Chatroom.kt @@ -24,7 +24,7 @@ import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder import com.vitorpamplona.quartz.nip01Core.HexKey -import com.vitorpamplona.quartz.nip04Dm.PrivateDmEvent +import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent import com.vitorpamplona.quartz.nip14Subject.subject import com.vitorpamplona.quartz.utils.TimeUtils 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 51047d5fcc..c7aee6dffd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -69,7 +69,7 @@ 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.nip04Dm.messages.PrivateDmEvent import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/Base64Image.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/Base64Image.kt index 91b3ad9dcc..41e5204c8a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/Base64Image.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/Base64Image.kt @@ -34,8 +34,8 @@ import coil3.key.Keyer import coil3.request.ImageRequest import coil3.request.Options import com.vitorpamplona.amethyst.commons.richtext.RichTextParser.Companion.base64contentPattern -import com.vitorpamplona.quartz.CryptoUtils import com.vitorpamplona.quartz.nip01Core.toHexKey +import com.vitorpamplona.quartz.utils.sha256 import java.util.Base64 @Stable @@ -83,7 +83,7 @@ class Base64Fetcher( options: Options, ): String? = if (data.scheme == "data") { - CryptoUtils.sha256(data.toString().toByteArray()).toHexKey() + sha256(data.toString().toByteArray()).toHexKey() } else { 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 2fd76043ae..130c17a379 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomDataSource.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomDataSource.kt @@ -25,7 +25,7 @@ import com.vitorpamplona.amethyst.service.relays.EOSEAccount 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.nip04Dm.messages.PrivateDmEvent import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey object NostrChatroomDataSource : AmethystNostrDataSource("ChatroomFeed") { 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 8ddd310fa5..37fc97c84b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomListDataSource.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomListDataSource.kt @@ -26,7 +26,7 @@ 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.nip04Dm.PrivateDmEvent +import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent 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 29b45f65a1..f41c8c7408 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrSearchEventOrUserDataSource.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrSearchEventOrUserDataSource.kt @@ -30,13 +30,13 @@ import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStory 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.crypto.KeyPair +import com.vitorpamplona.quartz.nip01Core.crypto.Nip01 +import com.vitorpamplona.quartz.nip01Core.hexToByteArray 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 import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser -import com.vitorpamplona.quartz.nip19Bech32.bech32.bechToBytes import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress import com.vitorpamplona.quartz.nip19Bech32.entities.NEmbed import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent @@ -80,7 +80,7 @@ object NostrSearchEventOrUserDataSource : AmethystNostrDataSource("SearchEventFe } when (val parsed = Nip19Parser.uriToRoute(mySearchString)?.entity) { - is NSec -> KeyPair(privKey = parsed.hex.bechToBytes()).pubKey.toHexKey() + is NSec -> Nip01.pubKeyCreate(parsed.hex.hexToByteArray()).toHexKey() is NPub -> parsed.hex is NProfile -> parsed.hex is com.vitorpamplona.quartz.nip19Bech32.entities.Note -> parsed.hex diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/OnlineCheck.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/OnlineCheck.kt index e00932f21c..1fd8570497 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/OnlineCheck.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/OnlineCheck.kt @@ -25,7 +25,7 @@ import android.util.LruCache import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager -import com.vitorpamplona.quartz.CryptoUtils +import com.vitorpamplona.quartz.utils.RandomInstance import okhttp3.EventListener import okhttp3.Protocol import okhttp3.Request @@ -70,7 +70,7 @@ object OnlineChecker { .url(url.replace("wss+livekit://", "wss://")) .header("Upgrade", "websocket") .header("Connection", "Upgrade") - .header("Sec-WebSocket-Key", CryptoUtils.random(16).toByteString().base64()) + .header("Sec-WebSocket-Key", RandomInstance.bytes(16).toByteString().base64()) .header("Sec-WebSocket-Version", "13") .header("Sec-WebSocket-Extensions", "permessage-deflate") .build() 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 991915cf5f..0d662a63d6 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 @@ -38,7 +38,7 @@ 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.taggedUserIds -import com.vitorpamplona.quartz.nip04Dm.PrivateDmEvent +import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent import com.vitorpamplona.quartz.nip19Bech32.toNpub 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 353e5d3cd0..28e40e032f 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 @@ -23,9 +23,9 @@ package com.vitorpamplona.amethyst.service.uploads import android.content.Context 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.files.encryption.NostrCipher +import com.vitorpamplona.quartz.utils.sha256 import java.io.File class EncryptFilesResult( @@ -48,9 +48,9 @@ class EncryptFiles { resolver.openInputStream(inputFile)!!.use { inputStream -> val bytes = inputStream.readBytes() - val originalHash = CryptoUtils.sha256(bytes).toHexKey() + val originalHash = sha256(bytes).toHexKey() val encrypted = cipher.encrypt(bytes) - val encryptedHash = CryptoUtils.sha256(encrypted).toHexKey() + val encryptedHash = sha256(encrypted).toHexKey() encryptedFile.outputStream().use { outputStream -> outputStream.write(encrypted) 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 092bcdc22f..e22655d38e 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 @@ -30,9 +30,9 @@ import android.os.Build import android.util.Log 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.tags.DimensionTag +import com.vitorpamplona.quartz.utils.sha256 import kotlinx.coroutines.CancellationException import java.io.IOException @@ -74,7 +74,7 @@ class FileHeader( dimPrecomputed: DimensionTag?, ): Result = try { - val hash = CryptoUtils.sha256(data).toHexKey() + val hash = sha256(data).toHexKey() val size = data.size val (blurHash, dim) = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt index 49cc0fc794..a6dabb114e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt @@ -35,10 +35,10 @@ import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager import com.vitorpamplona.amethyst.service.uploads.MediaUploadResult import com.vitorpamplona.amethyst.service.uploads.nip96.randomChars import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.quartz.CryptoUtils import com.vitorpamplona.quartz.blossom.BlossomAuthorizationEvent import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.toHexKey +import com.vitorpamplona.quartz.utils.sha256 import okhttp3.MediaType.Companion.toMediaType import okhttp3.Request import okhttp3.RequestBody @@ -88,7 +88,7 @@ class BlossomUploader { checkNotNull(payload) { "Can't open the image input stream" } - val hash = CryptoUtils.sha256(payload).toHexKey() + val hash = sha256(payload).toHexKey() val imageInputStream = contentResolver.openInputStream(uri) 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 ed4f4bee3a..8f363caa3b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt @@ -47,7 +47,7 @@ import com.vitorpamplona.amethyst.ui.screen.AccountScreen import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel import com.vitorpamplona.amethyst.ui.theme.AmethystTheme import com.vitorpamplona.amethyst.ui.tor.TorManager -import com.vitorpamplona.quartz.nip04Dm.PrivateDmEvent +import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress import com.vitorpamplona.quartz.nip19Bech32.entities.NEmbed 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 41816ec2a6..beb7bab1a8 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.crypto.KeyPair +import com.vitorpamplona.quartz.nip01Core.crypto.Nip01 import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser import com.vitorpamplona.quartz.nip19Bech32.bech32.Bech32 import com.vitorpamplona.quartz.nip19Bech32.bech32.bechToBytes @@ -180,7 +180,7 @@ class NewMessageTagger( val restOfWord = key.substring(63) // Converts to npub val pubkey = - Nip19Parser.uriToRoute(KeyPair(privKey = keyB32.bechToBytes()).pubKey.toNpub()) ?: return null + Nip19Parser.uriToRoute(Nip01.pubKeyCreate(keyB32.bechToBytes()).toNpub()) ?: return null return DirtyKeyInfo(pubkey, restOfWord.ifEmpty { null }) } else if (key.startsWith("npub1", true)) { 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 3f5be78752..e4bc3bfa33 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 @@ -77,7 +77,7 @@ 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.nip04Dm.messages.PrivateDmEvent import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip10Notes.content.findHashtags 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 11eb81bd46..dc3757935a 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 @@ -67,7 +67,7 @@ import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList import com.vitorpamplona.quartz.nip02FollowList.ImmutableListOfLists -import com.vitorpamplona.quartz.nip04Dm.PrivateDmEvent +import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress import com.vitorpamplona.quartz.nip19Bech32.entities.NEmbed 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 0cd971f287..721a8ca2fd 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 @@ -93,11 +93,11 @@ import com.vitorpamplona.amethyst.ui.theme.Size30dp import com.vitorpamplona.amethyst.ui.theme.Size75dp import com.vitorpamplona.amethyst.ui.theme.hashVerifierMark import com.vitorpamplona.amethyst.ui.theme.imageModifier -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.tags.DimensionTag +import com.vitorpamplona.quartz.utils.sha256 import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.Dispatchers @@ -695,7 +695,7 @@ private suspend fun verifyHash(content: MediaUrlContent): Boolean? { if (content.hash == null) return null Amethyst.instance.coilCache.openSnapshot(content.url)?.use { snapshot -> - val hash = CryptoUtils.sha256(snapshot.data.toFile().readBytes()).toHexKey() + val hash = sha256(snapshot.data.toFile().readBytes()).toHexKey() Log.d("Image Hash Verification", "$hash == ${content.hash}") 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 4ebefbe733..faee792da6 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 @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.ui.dal 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.nip04Dm.messages.PrivateDmEvent import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable 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 c72513d8e0..6061f6b34d 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 @@ -26,7 +26,7 @@ import com.vitorpamplona.amethyst.model.Note 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.nip04Dm.messages.PrivateDmEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent 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 d8db702760..1881067b1d 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 @@ -26,7 +26,7 @@ import com.vitorpamplona.amethyst.model.Note 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 +import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent 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 49d88fbacd..1f099290eb 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 @@ -160,7 +160,7 @@ 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.nip04Dm.messages.PrivateDmEvent import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip13Pow.strongPoWOrNull 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 b217cce080..6a0dd6bdbb 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 @@ -51,7 +51,7 @@ import com.vitorpamplona.amethyst.ui.theme.replyModifier 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.nip04Dm.messages.PrivateDmEvent import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable import com.vitorpamplona.quartz.nip19Bech32.toNpub 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 ee0ba65c80..b8f85b4f53 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 @@ -36,7 +36,6 @@ import com.vitorpamplona.amethyst.service.Nip05NostrAddressVerifier 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.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.hexToByteArray import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent @@ -45,6 +44,7 @@ import com.vitorpamplona.quartz.nip01Core.toHexKey import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import com.vitorpamplona.quartz.nip02FollowList.ContactTag import com.vitorpamplona.quartz.nip02FollowList.ReadWrite +import com.vitorpamplona.quartz.nip06KeyDerivation.Nip06 import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser import com.vitorpamplona.quartz.nip19Bech32.bech32.bechToBytes @@ -56,6 +56,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.toNpub +import com.vitorpamplona.quartz.nip49PrivKeyEnc.Nip49 import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.utils.Hex @@ -142,9 +143,9 @@ class AccountStateViewModel : ViewModel() { transientAccount = transientAccount, torSettings = TorSettingsFlow.build(torSettings), ) - } else if (key.contains(" ") && CryptoUtils.isValidMnemonic(key)) { + } else if (key.contains(" ") && Nip06().isValidMnemonic(key)) { AccountSettings( - keyPair = KeyPair(privKey = CryptoUtils.privateKeyFromMnemonic(key)), + keyPair = KeyPair(privKey = Nip06().privateKeyFromMnemonic(key)), transientAccount = transientAccount, torSettings = TorSettingsFlow.build(torSettings), ) @@ -206,7 +207,11 @@ class AccountStateViewModel : ViewModel() { if (key.startsWith("ncryptsec")) { val newKey = try { - CryptoUtils.decryptNIP49(key, password) + if (key.isEmpty() || password.isEmpty()) { + null + } else { + Nip49().decrypt(key, password) + } } catch (e: Exception) { if (e is CancellationException) throw e onError(e.message) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountBackupDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountBackupDialog.kt index a632bea326..5bfff3424d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountBackupDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountBackupDialog.kt @@ -102,9 +102,9 @@ import com.vitorpamplona.amethyst.ui.theme.ButtonPadding import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow import com.vitorpamplona.amethyst.ui.theme.grayText import com.vitorpamplona.amethyst.ui.theme.placeholderText -import com.vitorpamplona.quartz.CryptoUtils import com.vitorpamplona.quartz.nip01Core.toHexKey import com.vitorpamplona.quartz.nip19Bech32.toNsec +import com.vitorpamplona.quartz.nip49PrivKeyEnc.Nip49 import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -444,7 +444,7 @@ private fun encryptCopyNSec( } } else { accountViewModel.account.settings.keyPair.privKey?.let { - val key = CryptoUtils.encryptNIP49(it.toHexKey(), password.value.text) + val key = runCatching { Nip49().encrypt(it.toHexKey(), password.value.text) }.getOrNull() if (key != null) { clipboardManager.setText(AnnotatedString(key)) scope.launch { @@ -540,7 +540,7 @@ private fun QrCodeButtonEncrypted( onDialogShow = { accountViewModel.account.settings.keyPair.privKey ?.toHexKey() - ?.let { CryptoUtils.encryptNIP49(it, password.value.text) } + ?.let { Nip49().encrypt(it, password.value.text) } }, ) } 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 0c12b1f173..f14583fe40 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 @@ -111,8 +111,8 @@ import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.nip65RelayList.RelayUrlFormatter import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryResponseEvent import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag +import com.vitorpamplona.quartz.utils.Hex import com.vitorpamplona.quartz.utils.TimeUtils -import fr.acinq.secp256k1.Hex import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.persistentSetOf @@ -1753,7 +1753,6 @@ fun mockAccountViewModel(): AccountViewModel { KeyPair( privKey = Hex.decode("0f761f8a5a481e26f06605a1d9b3e9eba7a107d351f43c43a57469b788274499"), pubKey = Hex.decode("989c3734c46abac7ce3ce229971581a5a6ee39cdd6aa7261a55823fa7f8c4799"), - forcePubKeyCheck = false, ), ), sharedPreferencesViewModel.sharedPrefs, @@ -1771,7 +1770,6 @@ fun mockVitorAccountViewModel(): AccountViewModel { keyPair = KeyPair( pubKey = Hex.decode("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"), - forcePubKeyCheck = false, ), ), sharedPreferencesViewModel.sharedPrefs, 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 30737d867b..4421d1649e 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 @@ -104,7 +104,7 @@ import com.vitorpamplona.amethyst.ui.theme.placeholderText 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.nip04Dm.messages.PrivateDmEvent import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable import com.vitorpamplona.quartz.nip17Dm.base.NIP17Group import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent 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 0f9e94663e..7bb7a6da65 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 @@ -38,7 +38,7 @@ import com.vitorpamplona.amethyst.ui.feeds.InvalidatableContent 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.nip04Dm.messages.PrivateDmEvent import com.vitorpamplona.quartz.nip17Dm.base.NIP17Group import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent 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 c16820c484..5d4218e43d 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 @@ -175,7 +175,7 @@ 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.nip04Dm.messages.PrivateDmEvent import com.vitorpamplona.quartz.nip13Pow.strongPoWOrNull import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent 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 c3921abce0..ef2aeca89b 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/EventBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/EventBenchmark.kt @@ -65,11 +65,20 @@ class EventBenchmark { } @Test - fun eventFactoryPerformanceTest() { + fun eventFactoryKind1PerformanceTest() { val now = TimeUtils.now() val tags = arrayOf(arrayOf("")) benchmarkRule.measureRepeated { EventFactory.create("id", "pubkey", now, 1, tags, "content", "sig") } } + + @Test + fun eventFactoryKind30818PerformanceTest() { + val now = TimeUtils.now() + val tags = arrayOf(arrayOf("")) + benchmarkRule.measureRepeated { + EventFactory.create("id", "pubkey", now, 30818, tags, "content", "sig") + } + } } 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 024436e105..33493df011 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapBenchmark.kt @@ -23,7 +23,6 @@ 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.CryptoUtils import com.vitorpamplona.quartz.nip01Core.checkSignature import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair @@ -132,7 +131,6 @@ class GiftWrapBenchmark { // Simulate Receiver benchmarkRule.measureRepeated { - CryptoUtils.clearCache() val counter = CountDownLatch(1) val wrap = Event.fromJson(giftWrapJson) as GiftWrapEvent 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 d1fc02a318..00900de95f 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapReceivingBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapReceivingBenchmark.kt @@ -23,7 +23,6 @@ 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.CryptoUtils import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.hexToByteArray @@ -35,6 +34,7 @@ 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.nip44Encryption.Nip44 import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiser import com.vitorpamplona.quartz.nip59Giftwrap.rumors.Rumor import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent @@ -171,7 +171,7 @@ class GiftWrapReceivingBenchmark { benchmarkRule.measureRepeated { assertNotNull( - CryptoUtils.decryptNIP44( + Nip44.decrypt( wrap.content, receiver.keyPair.privKey!!, wrap.pubKey.hexToByteArray(), @@ -188,7 +188,7 @@ class GiftWrapReceivingBenchmark { val wrap = createWrap(sender, receiver) val innerJson = - CryptoUtils.decryptNIP44( + Nip44.decrypt( wrap.content, receiver.keyPair.privKey!!, wrap.pubKey.hexToByteArray(), @@ -206,7 +206,7 @@ class GiftWrapReceivingBenchmark { benchmarkRule.measureRepeated { assertNotNull( - CryptoUtils.decryptNIP44( + Nip44.decrypt( seal.content, receiver.keyPair.privKey!!, seal.pubKey.hexToByteArray(), @@ -223,7 +223,7 @@ class GiftWrapReceivingBenchmark { val seal = createSeal(sender, receiver) val innerJson = - CryptoUtils.decryptNIP44( + Nip44.decrypt( seal.content, receiver.keyPair.privKey!!, seal.pubKey.hexToByteArray(), diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/Sha256Benchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/Sha256Benchmark.kt index 3fbd26471d..5885c22369 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/Sha256Benchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/Sha256Benchmark.kt @@ -25,7 +25,7 @@ import androidx.benchmark.junit4.measureRepeated import androidx.test.ext.junit.runners.AndroidJUnit4 import com.vitorpamplona.quartz.nip01Core.EventHasher import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.utils.sha256Hash +import com.vitorpamplona.quartz.utils.sha256 import junit.framework.TestCase.assertNotNull import org.junit.Rule import org.junit.Test @@ -49,7 +49,7 @@ class Sha256Benchmark { benchmarkRule.measureRepeated { // Should pass - assertNotNull(sha256Hash(byteArray)) + assertNotNull(sha256(byteArray)) } } } diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/CryptoBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/SharedKeyBenchmark.kt similarity index 63% rename from benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/CryptoBenchmark.kt rename to benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/SharedKeyBenchmark.kt index a29c3ace4f..2a2a9d3225 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/CryptoBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/SharedKeyBenchmark.kt @@ -23,15 +23,17 @@ 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.CryptoUtils import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair +import com.vitorpamplona.quartz.nip04Dm.crypto.Encryption +import com.vitorpamplona.quartz.nip04Dm.crypto.Nip04 +import com.vitorpamplona.quartz.nip44Encryption.Nip44v2 import junit.framework.TestCase.assertNotNull import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) -class CryptoBenchmark { +class SharedKeyBenchmark { @get:Rule val benchmarkRule = BenchmarkRule() @Test @@ -40,7 +42,7 @@ class CryptoBenchmark { val keyPair2 = KeyPair() benchmarkRule.measureRepeated { - assertNotNull(CryptoUtils.getSharedSecretNIP04(keyPair1.privKey!!, keyPair2.pubKey)) + assertNotNull(Nip04.getSharedSecret(keyPair1.privKey!!, keyPair2.pubKey)) } } @@ -48,9 +50,10 @@ class CryptoBenchmark { fun getSharedKeyNip44() { val keyPair1 = KeyPair() val keyPair2 = KeyPair() + val nip44v2 = Nip44v2() benchmarkRule.measureRepeated { - assertNotNull(CryptoUtils.nip44.v1.getSharedSecret(keyPair1.privKey!!, keyPair2.pubKey)) + assertNotNull(nip44v2.getConversationKey(keyPair1.privKey!!, keyPair2.pubKey)) } } @@ -58,9 +61,10 @@ class CryptoBenchmark { fun computeSharedKeyNip04() { val keyPair1 = KeyPair() val keyPair2 = KeyPair() + val nip04 = Encryption() benchmarkRule.measureRepeated { - assertNotNull(CryptoUtils.computeSharedSecretNIP04(keyPair1.privKey!!, keyPair2.pubKey)) + assertNotNull(nip04.computeSharedSecret(keyPair1.privKey!!, keyPair2.pubKey)) } } @@ -68,40 +72,10 @@ class CryptoBenchmark { fun computeSharedKeyNip44() { val keyPair1 = KeyPair() val keyPair2 = KeyPair() + val nip44v2 = Nip44v2() benchmarkRule.measureRepeated { - assertNotNull(CryptoUtils.nip44.v1.computeSharedSecret(keyPair1.privKey!!, keyPair2.pubKey)) - } - } - - @Test - fun random() { - benchmarkRule.measureRepeated { assertNotNull(CryptoUtils.random(1000)) } - } - - @Test - fun sha256() { - val keyPair = KeyPair() - - benchmarkRule.measureRepeated { assertNotNull(CryptoUtils.sha256(keyPair.pubKey)) } - } - - @Test - fun sign() { - val keyPair = KeyPair() - val msg = CryptoUtils.sha256(CryptoUtils.random(1000)) - - benchmarkRule.measureRepeated { assertNotNull(CryptoUtils.sign(msg, keyPair.privKey!!)) } - } - - @Test - fun verify() { - val keyPair = KeyPair() - val msg = CryptoUtils.sha256(CryptoUtils.random(1000)) - val signature = CryptoUtils.sign(msg, keyPair.privKey!!) - - benchmarkRule.measureRepeated { - assertNotNull(CryptoUtils.verifySignature(signature, msg, keyPair.pubKey)) + assertNotNull(nip44v2.computeConversationKey(keyPair1.privKey!!, keyPair2.pubKey)) } } } diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/SignVerifyBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/SignVerifyBenchmark.kt new file mode 100644 index 0000000000..01dbf62a45 --- /dev/null +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/SignVerifyBenchmark.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.benchmark + +import androidx.benchmark.junit4.BenchmarkRule +import androidx.benchmark.junit4.measureRepeated +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair +import com.vitorpamplona.quartz.nip01Core.crypto.Nip01 +import com.vitorpamplona.quartz.utils.RandomInstance +import com.vitorpamplona.quartz.utils.sha256 +import junit.framework.TestCase.assertNotNull +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class SignVerifyBenchmark { + @get:Rule val benchmarkRule = BenchmarkRule() + + @Test + fun random() { + benchmarkRule.measureRepeated { assertNotNull(RandomInstance.bytes(1000)) } + } + + @Test + fun sign() { + val keyPair = KeyPair() + val msg = sha256(RandomInstance.bytes(1000)) + + benchmarkRule.measureRepeated { assertNotNull(Nip01.sign(msg, keyPair.privKey!!)) } + } + + @Test + fun verify() { + val keyPair = KeyPair() + val msg = sha256(RandomInstance.bytes(1000)) + val signature = Nip01.sign(msg, keyPair.privKey!!) + + benchmarkRule.measureRepeated { + assertNotNull(Nip01.verify(signature, msg, keyPair.pubKey)) + } + } +} diff --git a/commons/src/main/java/com/vitorpamplona/amethyst/commons/robohash/RobohashAssembler.kt b/commons/src/main/java/com/vitorpamplona/amethyst/commons/robohash/RobohashAssembler.kt index a07bb1f8db..eab71fe674 100644 --- a/commons/src/main/java/com/vitorpamplona/amethyst/commons/robohash/RobohashAssembler.kt +++ b/commons/src/main/java/com/vitorpamplona/amethyst/commons/robohash/RobohashAssembler.kt @@ -85,8 +85,8 @@ import com.vitorpamplona.amethyst.commons.robohash.parts.mouth6Cell import com.vitorpamplona.amethyst.commons.robohash.parts.mouth7Happy import com.vitorpamplona.amethyst.commons.robohash.parts.mouth8Buttons import com.vitorpamplona.amethyst.commons.robohash.parts.mouth9Closed -import com.vitorpamplona.quartz.CryptoUtils import com.vitorpamplona.quartz.utils.Hex +import com.vitorpamplona.quartz.utils.sha256 val Black = SolidColor(Color.Black) val Gray = SolidColor(Color(0xFF6d6e70)) @@ -168,7 +168,7 @@ class RobohashAssembler { Hex.decode(msg) } else { Log.w("Robohash", "$msg is not a hex") - CryptoUtils.sha256(msg.toByteArray()) + sha256(msg.toByteArray()) } val bgColor = SolidColor(bytesToColor(hash[0], hash[1], hash[2], isLightTheme)) diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/CryptoUtilsTest.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/CryptoUtilsTest.kt deleted file mode 100644 index 2b5b4862b3..0000000000 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/CryptoUtilsTest.kt +++ /dev/null @@ -1,140 +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 - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair -import com.vitorpamplona.quartz.nip01Core.hexToByteArray -import com.vitorpamplona.quartz.nip01Core.toHexKey -import org.junit.Assert.assertEquals -import org.junit.Test -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class CryptoUtilsTest { - @Test - fun testGetPublicFromPrivateKey() { - val privateKey = - "f410f88bcec6cbfda04d6a273c7b1dd8bba144cd45b71e87109cfa11dd7ed561".hexToByteArray() - val publicKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey() - assertEquals("7d4b8806f1fd713c287235411bf95aa81b7242ead892733ec84b3f2719845be6", publicKey) - } - - @Test - fun testSharedSecretCompatibilityWithCoracle() { - val privateKey = "f410f88bcec6cbfda04d6a273c7b1dd8bba144cd45b71e87109cfa11dd7ed561" - val publicKey = "765cd7cf91d3ad07423d114d5a39c61d52b2cdbc18ba055ddbbeec71fbe2aa2f" - - val key = - CryptoUtils.nip44.v1.getSharedSecret( - privateKey = privateKey.hexToByteArray(), - pubKey = publicKey.hexToByteArray(), - ) - - assertEquals("577c966f499dddd8e8dcc34e8f352e283cc177e53ae372794947e0b8ede7cfd8", key.toHexKey()) - } - - @Test - fun testSharedSecret() { - val sender = KeyPair() - val receiver = KeyPair() - - val sharedSecret1 = CryptoUtils.nip44.v1.getSharedSecret(sender.privKey!!, receiver.pubKey) - val sharedSecret2 = CryptoUtils.nip44.v1.getSharedSecret(receiver.privKey!!, sender.pubKey) - - assertEquals(sharedSecret1.toHexKey(), sharedSecret2.toHexKey()) - - val secretKey1 = KeyPair(privKey = sharedSecret1) - val secretKey2 = KeyPair(privKey = sharedSecret2) - - assertEquals(secretKey1.pubKey.toHexKey(), secretKey2.pubKey.toHexKey()) - assertEquals(secretKey1.privKey?.toHexKey(), secretKey2.privKey?.toHexKey()) - } - - @Test - fun encryptDecryptNIP4Test() { - val msg = "Hi" - - val privateKey = CryptoUtils.privkeyCreate() - val publicKey = CryptoUtils.pubkeyCreate(privateKey) - - val encrypted = CryptoUtils.encryptNIP04(msg, privateKey, publicKey) - val decrypted = CryptoUtils.decryptNIP04(encrypted, privateKey, publicKey) - - assertEquals(msg, decrypted) - } - - @Test - fun encryptDecryptNIP44v1Test() { - val msg = "Hi" - - val privateKey = CryptoUtils.privkeyCreate() - val publicKey = CryptoUtils.pubkeyCreate(privateKey) - - val encrypted = CryptoUtils.nip44.v1.encrypt(msg, privateKey, publicKey) - val decrypted = CryptoUtils.nip44.v1.decrypt(encrypted, privateKey, publicKey) - - assertEquals(msg, decrypted) - } - - @Test - fun encryptSharedSecretDecryptNIP4Test() { - val msg = "Hi" - - val privateKey = CryptoUtils.privkeyCreate() - val publicKey = CryptoUtils.pubkeyCreate(privateKey) - - val encrypted = CryptoUtils.encryptNIP04(msg, privateKey, publicKey) - val decrypted = CryptoUtils.decryptNIP04(encrypted, privateKey, publicKey) - - assertEquals(msg, decrypted) - } - - @Test - fun encryptSharedSecretDecryptNIP44v1Test() { - val msg = "Hi" - - val privateKey = CryptoUtils.privkeyCreate() - val publicKey = CryptoUtils.pubkeyCreate(privateKey) - val sharedSecret = CryptoUtils.nip44.v1.getSharedSecret(privateKey, publicKey) - - val encrypted = CryptoUtils.nip44.v1.encrypt(msg, sharedSecret) - val decrypted = CryptoUtils.nip44.v1.decrypt(encrypted, sharedSecret) - - assertEquals(msg, decrypted) - } - - @Test - fun signString() { - val random = "319cc5596fdd6cd767e5a59d976e8e059c61306af90dff1e6ee1067b3a1fdbc0".hexToByteArray() - val message = "8e58c8251bb406b6ded69e9eb14f55282a9a53bdab16fc49a3218c2ad3abc887".hexToByteArray() - val keyPair = KeyPair("a5ab474552c8f9c46c2eda5a0b68f27430ad81f96cb405e0cb4e34bf0c6494a2".hexToByteArray()) - - val signedMessage = CryptoUtils.sign(message, keyPair.privKey!!, random).toHexKey() - val expectedValue = "0f9be7e01ba53d5ee6874b9180c7956269fda7a5be424634c3d17b5cfcea6da001be89183876415ba08b7dafa6cff4555e393dc228fb8769b384344e9a27b77c" - assertEquals(expectedValue, signedMessage) - - val message2 = "Hello" - val signedMessage2 = CryptoUtils.signString(message2, keyPair.privKey!!, random).toHexKey() - val expectedValue2 = "7ec8194a585bfb513564113b6b7bfeaafa0254c99d24eaf92280657c2291bab908b1b7bc553c83276a0254aef5041bbe6a50e93381edc4de3d859efa1c3a5a1e" - assertEquals(expectedValue2, signedMessage2) - } -} diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/bloom/BloomFilter.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/bloom/BloomFilter.kt index d41e4d6284..1a9b1bacdf 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/bloom/BloomFilter.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/bloom/BloomFilter.kt @@ -21,9 +21,10 @@ package com.vitorpamplona.quartz.bloom import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.vitorpamplona.quartz.CryptoUtils import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.crypto.Nip01 import com.vitorpamplona.quartz.nip01Core.hexToByteArray +import com.vitorpamplona.quartz.utils.RandomInstance import junit.framework.TestCase.assertEquals import junit.framework.TestCase.assertFalse import junit.framework.TestCase.assertTrue @@ -41,7 +42,7 @@ class BloomFilter( private val size: Int, private val rounds: Int, private val bits: BitSet = BitSet(size), - private val salt: ByteArray = CryptoUtils.random(8), + private val salt: ByteArray = RandomInstance.bytes(8), ) { private val hash = MessageDigest.getInstance("SHA-256") private val lock = ReentrantReadWriteLock() @@ -135,7 +136,7 @@ class BloomFilterTest { var failureCounter = 0 for (seed in 0..1000000) { - if (bloomFilter.mightContains(CryptoUtils.pubkeyCreate(CryptoUtils.privkeyCreate()))) { + if (bloomFilter.mightContains(Nip01.pubKeyCreate(Nip01.privKeyCreate()))) { failureCounter++ } } 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 ce34592c76..7566c29ea2 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip01Core/Nip01Test.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip01Core/Nip01Test.kt @@ -22,25 +22,23 @@ 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 com.vitorpamplona.quartz.utils.Secp256k1Instance +import com.vitorpamplona.quartz.utils.sha256 import org.junit.Assert.assertEquals import org.junit.Assert.assertNotEquals import org.junit.Assert.assertTrue import org.junit.Test import org.junit.runner.RunWith -import java.security.SecureRandom @RunWith(AndroidJUnit4::class) class Nip01Test { - private val nip01 = Nip01(Secp256k1.get(), SecureRandom()) private val privateKey = "f410f88bcec6cbfda04d6a273c7b1dd8bba144cd45b71e87109cfa11dd7ed561".hexToByteArray() @Test fun testGetPublicFromPrivateKey() { assertEquals( "7d4b8806f1fd713c287235411bf95aa81b7242ead892733ec84b3f2719845be6", - nip01.pubkeyCreate(privateKey).toHexKey(), + Nip01.pubKeyCreate(privateKey).toHexKey(), ) } @@ -49,7 +47,7 @@ class Nip01Test { val key = "e6159851715b4aa6190c22b899b0c792847de0a4435ac5b678f35738351c43b0".hexToByteArray() assertEquals( "029fa4ce8c87ca546b196e6518db80a6780e1bd5552b61f9f17bafee5d4e34e09b", - nip01.compressedPubkeyCreate(key).toHexKey(), + Secp256k1Instance.compressedPubKeyFor(key).toHexKey(), ) } @@ -58,7 +56,7 @@ class Nip01Test { val key = "65f039136f8da8d3e87b4818746b53318d5481e24b2673f162815144223a0b5a".hexToByteArray() assertEquals( "033dcef7585efbdb68747d919152bd481e21f5e952aaaef5a19604fbd096a93dd5", - nip01.compressedPubkeyCreate(key).toHexKey(), + Secp256k1Instance.compressedPubKeyFor(key).toHexKey(), ) } @@ -66,7 +64,7 @@ class Nip01Test { fun testDeterministicSign() { assertEquals( "1484d0e0bd62165e822e31f1f4cc8e1ce8e20c30a060e24fb0ecd7baf7c624f661fb7a3e4f0ddb43018e5f0b4892c929af64d8b7a86021aa081ec8231e3dfa37", - nip01.signDeterministic(sha256Hash("Test".toByteArray()), privateKey).toHexKey(), + Nip01.sign(sha256("Test".toByteArray()), privateKey, null).toHexKey(), ) } @@ -74,17 +72,17 @@ class Nip01Test { fun testSha256() { assertEquals( "532eaabd9574880dbf76b9b8cc00832c20a6ec113d682299550d7a6e0f345e25", - sha256Hash("Test".toByteArray()).toHexKey(), + sha256("Test".toByteArray()).toHexKey(), ) } @Test fun testDeterministicVerify() { assertTrue( - nip01.verify( + Nip01.verify( "1484d0e0bd62165e822e31f1f4cc8e1ce8e20c30a060e24fb0ecd7baf7c624f661fb7a3e4f0ddb43018e5f0b4892c929af64d8b7a86021aa081ec8231e3dfa37".hexToByteArray(), - sha256Hash("Test".toByteArray()), - nip01.pubkeyCreate(privateKey), + sha256("Test".toByteArray()), + Nip01.pubKeyCreate(privateKey), ), ) } @@ -93,18 +91,18 @@ class Nip01Test { fun testNonDeterministicSign() { assertNotEquals( "1484d0e0bd62165e822e31f1f4cc8e1ce8e20c30a060e24fb0ecd7baf7c624f661fb7a3e4f0ddb43018e5f0b4892c929af64d8b7a86021aa081ec8231e3dfa37", - nip01.sign(sha256Hash("Test".toByteArray()), privateKey).toHexKey(), + Nip01.sign(sha256("Test".toByteArray()), privateKey).toHexKey(), ) } @Test fun testNonDeterministicSignVerify() { - val signature = nip01.sign(sha256Hash("Test".toByteArray()), privateKey) + val signature = Nip01.sign(sha256("Test".toByteArray()), privateKey) assertTrue( - nip01.verify( + Nip01.verify( signature, - sha256Hash("Test".toByteArray()), - nip01.pubkeyCreate(privateKey), + sha256("Test".toByteArray()), + Nip01.pubKeyCreate(privateKey), ), ) } diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip01Core/crypto/Nip01CryptoTest.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip01Core/crypto/Nip01CryptoTest.kt new file mode 100644 index 0000000000..9835f3b0f5 --- /dev/null +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip01Core/crypto/Nip01CryptoTest.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.crypto + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.vitorpamplona.quartz.nip01Core.hexToByteArray +import com.vitorpamplona.quartz.nip01Core.toHexKey +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class Nip01CryptoTest { + @Test + fun testGetPublicFromPrivateKey() { + val privateKey = + "f410f88bcec6cbfda04d6a273c7b1dd8bba144cd45b71e87109cfa11dd7ed561".hexToByteArray() + val publicKey = Nip01.pubKeyCreate(privateKey).toHexKey() + assertEquals("7d4b8806f1fd713c287235411bf95aa81b7242ead892733ec84b3f2719845be6", publicKey) + } +} diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip04Dm/Nip04EncryptionTest.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip04Dm/EncryptionTest.kt similarity index 77% rename from quartz/src/androidTest/java/com/vitorpamplona/quartz/nip04Dm/Nip04EncryptionTest.kt rename to quartz/src/androidTest/java/com/vitorpamplona/quartz/nip04Dm/EncryptionTest.kt index d024ca17ac..e25755e291 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip04Dm/Nip04EncryptionTest.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip04Dm/EncryptionTest.kt @@ -23,21 +23,19 @@ package com.vitorpamplona.quartz.nip04Dm import com.vitorpamplona.quartz.nip01Core.crypto.Nip01 import com.vitorpamplona.quartz.nip01Core.hexToByteArray import com.vitorpamplona.quartz.nip01Core.toHexKey -import fr.acinq.secp256k1.Secp256k1 +import com.vitorpamplona.quartz.nip04Dm.crypto.EncryptedInfo +import com.vitorpamplona.quartz.nip04Dm.crypto.Encryption import junit.framework.TestCase.assertEquals import junit.framework.TestCase.assertTrue import org.junit.Test -import java.security.SecureRandom -class Nip04EncryptionTest { - private val random = SecureRandom() - private val nip01 = Nip01(Secp256k1.get(), random) - private val nip04 = Nip04Encryption(Secp256k1.get(), random) +class EncryptionTest { + private val nip04 = Encryption() val sk1 = "91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe".hexToByteArray() val sk2 = "96f6fa197aa07477ab88f6981118466ae3a982faab8ad5db9d5426870c73d220".hexToByteArray() - val pk1 = nip01.pubkeyCreate(sk1) - val pk2 = nip01.pubkeyCreate(sk2) + val pk1 = Nip01.pubKeyCreate(sk1) + val pk2 = Nip01.pubKeyCreate(sk2) val expectedShared = "7ce22696eb0e303ddaa491bdf2a56b79d249f2d861b8e012a933e01dc4beba81" @@ -89,17 +87,17 @@ class Nip04EncryptionTest { @Test fun isNIP04Encode() { - assertTrue(Nip04Encryption.isNIP04("Xj/oZZolaItdyQ5v7xYFpA==?iv=+a6zagBp+mr5m1aFbHQ8lA==")) - assertTrue(Nip04Encryption.isNIP04("zJxfaJ32rN5Dg1ODjOlEew==?iv=EV5bUjcc4OX2Km/zPp4ndQ==")) + assertTrue(EncryptedInfo.isNIP04("Xj/oZZolaItdyQ5v7xYFpA==?iv=+a6zagBp+mr5m1aFbHQ8lA==")) + assertTrue(EncryptedInfo.isNIP04("zJxfaJ32rN5Dg1ODjOlEew==?iv=EV5bUjcc4OX2Km/zPp4ndQ==")) assertTrue( - 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=="), + EncryptedInfo.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( - Nip04Encryption.isNIP04( + EncryptedInfo.isNIP04( "QOAYBWa88ConWs2C4kSvNqAcowCtg0ZRtAl7FyLSv9VMaJH4oCiDx0h8VLBnV97HdE4lv" + "TW7AYC1eEw8/t1dbe0qRc3XrOt7MrPAO8yqpy1/3lFB1+10kip0+KdgT8Quvv02wTP8Dqi" + "xpr2fliAIG2ONvDn+O5V0q9aVUN9HitgL/myTyR0T42edmxWeZoMBEOKvJyO80FekSsgVL" + diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip04Dm/Nip04Test.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip04Dm/Nip04Test.kt new file mode 100644 index 0000000000..29e0acfc8f --- /dev/null +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip04Dm/Nip04Test.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.nip04Dm + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.vitorpamplona.quartz.nip01Core.crypto.Nip01 +import com.vitorpamplona.quartz.nip04Dm.crypto.Nip04 +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class Nip04Test { + @Test + fun encryptDecryptNIP4Test() { + val msg = "Hi" + + val privateKey = Nip01.privKeyCreate() + val publicKey = Nip01.pubKeyCreate(privateKey) + + val encrypted = Nip04.encrypt(msg, privateKey, publicKey) + val decrypted = Nip04.decrypt(encrypted, privateKey, publicKey) + + assertEquals(msg, decrypted) + } + + @Test + fun encryptSharedSecretDecryptNIP4Test() { + val msg = "Hi" + + val privateKey = Nip01.privKeyCreate() + val publicKey = Nip01.pubKeyCreate(privateKey) + + val encrypted = Nip04.encrypt(msg, privateKey, publicKey) + val decrypted = Nip04.decrypt(encrypted, privateKey, publicKey) + + assertEquals(msg, decrypted) + } +} diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip06KeyDerivation/Bip32SeedDerivationTest.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip06KeyDerivation/Bip32SeedDerivationTest.kt index a1fef7a1da..722372b3c1 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip06KeyDerivation/Bip32SeedDerivationTest.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip06KeyDerivation/Bip32SeedDerivationTest.kt @@ -22,14 +22,13 @@ package com.vitorpamplona.quartz.nip06KeyDerivation import androidx.test.ext.junit.runners.AndroidJUnit4 import com.vitorpamplona.quartz.nip01Core.toHexKey -import fr.acinq.secp256k1.Secp256k1 import junit.framework.TestCase.assertEquals import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class Bip32SeedDerivationTest { - val seedDerivation = Bip32SeedDerivation(Secp256k1.get()) + val seedDerivation = Bip32SeedDerivation() val masterBitcoin = seedDerivation.generate( diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip06KeyDerivation/Bip39MnemonicsTest.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip06KeyDerivation/Bip39MnemonicsTest.kt index 1aaa0eaa5b..2e1d957296 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip06KeyDerivation/Bip39MnemonicsTest.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip06KeyDerivation/Bip39MnemonicsTest.kt @@ -23,9 +23,9 @@ package com.vitorpamplona.quartz.nip06KeyDerivation import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -import com.vitorpamplona.quartz.CryptoUtils import com.vitorpamplona.quartz.nip01Core.toHexKey import com.vitorpamplona.quartz.utils.Hex +import com.vitorpamplona.quartz.utils.RandomInstance import junit.framework.TestCase.assertEquals import junit.framework.TestCase.assertTrue import junit.framework.TestCase.fail @@ -69,7 +69,7 @@ class Bip39MnemonicsTest { fun validateMnemonicsValid() { for (i in 0..99) { for (length in listOf(16, 20, 24, 28, 32, 36, 40)) { - val mnemonics = Bip39Mnemonics.toMnemonics(CryptoUtils.random(length)) + val mnemonics = Bip39Mnemonics.toMnemonics(RandomInstance.bytes(length)) Bip39Mnemonics.validate(mnemonics) } } diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip06KeyDerivation/Nip06Test.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip06KeyDerivation/Nip06Test.kt index 2466e84da8..040cfa7581 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip06KeyDerivation/Nip06Test.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip06KeyDerivation/Nip06Test.kt @@ -22,7 +22,6 @@ package com.vitorpamplona.quartz.nip06KeyDerivation import androidx.test.ext.junit.runners.AndroidJUnit4 import com.vitorpamplona.quartz.nip01Core.toHexKey -import fr.acinq.secp256k1.Secp256k1 import junit.framework.TestCase.assertEquals import org.junit.Ignore import org.junit.Test @@ -30,7 +29,7 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class Nip06Test { - val nip06 = Nip06(Secp256k1.get()) + val nip06 = Nip06() // private key (hex): 7f7ff03d123792d6ac594bfa67bf6d0c0ab55b6b1fdb6249303fe861f1ccba9a // nsec: nsec10allq0gjx7fddtzef0ax00mdps9t2kmtrldkyjfs8l5xruwvh2dq0lhhkp diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip44Encryption/Nip44v1Test.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip44Encryption/Nip44v1Test.kt new file mode 100644 index 0000000000..a3bc8b6453 --- /dev/null +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip44Encryption/Nip44v1Test.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.nip44Encryption + +import androidx.test.ext.junit.runners.AndroidJUnit4 +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 org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class Nip44v1Test { + private val nip44v1 = Nip44v1() + + @Test + fun testSharedSecretCompatibilityWithCoracle() { + val privateKey = "f410f88bcec6cbfda04d6a273c7b1dd8bba144cd45b71e87109cfa11dd7ed561" + val publicKey = "765cd7cf91d3ad07423d114d5a39c61d52b2cdbc18ba055ddbbeec71fbe2aa2f" + + val key = + nip44v1.getSharedSecret( + privateKey = privateKey.hexToByteArray(), + pubKey = publicKey.hexToByteArray(), + ) + + assertEquals("577c966f499dddd8e8dcc34e8f352e283cc177e53ae372794947e0b8ede7cfd8", key.toHexKey()) + } + + @Test + fun testSharedSecret() { + val sender = KeyPair() + val receiver = KeyPair() + + val sharedSecret1 = nip44v1.getSharedSecret(sender.privKey!!, receiver.pubKey) + val sharedSecret2 = nip44v1.getSharedSecret(receiver.privKey!!, sender.pubKey) + + assertEquals(sharedSecret1.toHexKey(), sharedSecret2.toHexKey()) + + val secretKey1 = KeyPair(privKey = sharedSecret1) + val secretKey2 = KeyPair(privKey = sharedSecret2) + + assertEquals(secretKey1.pubKey.toHexKey(), secretKey2.pubKey.toHexKey()) + assertEquals(secretKey1.privKey?.toHexKey(), secretKey2.privKey?.toHexKey()) + } + + @Test + fun encryptDecrypt() { + val msg = "Hi" + + val privateKey = Nip01.privKeyCreate() + val publicKey = Nip01.pubKeyCreate(privateKey) + + val encrypted = nip44v1.encrypt(msg, privateKey, publicKey) + val decrypted = nip44v1.decrypt(encrypted, privateKey, publicKey) + + assertEquals(msg, decrypted) + } + + @Test + fun encryptDecryptSharedSecret() { + val msg = "Hi" + + val privateKey = Nip01.privKeyCreate() + val publicKey = Nip01.pubKeyCreate(privateKey) + + val sharedSecret = nip44v1.getSharedSecret(privateKey, publicKey) + + val encrypted = nip44v1.encrypt(msg, sharedSecret) + val decrypted = nip44v1.decrypt(encrypted, sharedSecret) + + assertEquals(msg, decrypted) + } +} diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip44Encryption/NIP44v2Test.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip44Encryption/Nip44v2Test.kt similarity index 90% rename from quartz/src/androidTest/java/com/vitorpamplona/quartz/nip44Encryption/NIP44v2Test.kt rename to quartz/src/androidTest/java/com/vitorpamplona/quartz/nip44Encryption/Nip44v2Test.kt index f603c98d35..104371cddf 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip44Encryption/NIP44v2Test.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip44Encryption/Nip44v2Test.kt @@ -27,18 +27,17 @@ 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 -import fr.acinq.secp256k1.Secp256k1 +import com.vitorpamplona.quartz.utils.RandomInstance +import com.vitorpamplona.quartz.utils.sha256 import junit.framework.TestCase.assertEquals import junit.framework.TestCase.assertNotNull import junit.framework.TestCase.assertNull import junit.framework.TestCase.fail import org.junit.Test import org.junit.runner.RunWith -import java.security.SecureRandom @RunWith(AndroidJUnit4::class) -class NIP44v2Test { +class Nip44v2Test { private val vectors: VectorFile = jacksonObjectMapper() .readValue( @@ -46,9 +45,7 @@ class NIP44v2Test { VectorFile::class.java, ) - private val random = SecureRandom() - private val nip44v2 = Nip44v2(Secp256k1.get(), random) - private val nip01 = Nip01(Secp256k1.get(), random) + private val nip44v2 = Nip44v2() @Test fun conversationKeyTest() { @@ -73,8 +70,8 @@ class NIP44v2Test { val privateKeyA = "f410f88bcec6cbfda04d6a273c7b1dd8bba144cd45b71e87109cfa11dd7ed561".hexToByteArray() val privateKeyB = "65f039136f8da8d3e87b4818746b53318d5481e24b2673f162815144223a0b5a".hexToByteArray() - val publicKeyA = nip01.pubkeyCreate(privateKeyA) - val publicKeyB = nip01.pubkeyCreate(privateKeyB) + val publicKeyA = Nip01.pubKeyCreate(privateKeyA) + val publicKeyB = Nip01.pubKeyCreate(privateKeyB) assertEquals( nip44v2.getConversationKey(privateKeyA, publicKeyB).toHexKey(), @@ -87,8 +84,8 @@ class NIP44v2Test { val privateKeyA = "f410f88bcec6cbfda04d6a273c7b1dd8bba144cd45b71e87109cfa11dd7ed561".hexToByteArray() val privateKeyB = "e6159851715b4aa6190c22b899b0c792847de0a4435ac5b678f35738351c43b0".hexToByteArray() - val publicKeyA = nip01.pubkeyCreate(privateKeyA) - val publicKeyB = nip01.pubkeyCreate(privateKeyB) + val publicKeyA = Nip01.pubKeyCreate(privateKeyA) + val publicKeyB = Nip01.pubKeyCreate(privateKeyB) assertEquals( nip44v2.getConversationKey(privateKeyA, publicKeyB).toHexKey(), @@ -149,8 +146,7 @@ class NIP44v2Test { @Test fun invalidMessageLengths() { for (v in vectors.v2?.invalid?.encryptMsgLengths!!) { - val key = ByteArray(32) - random.nextBytes(key) + val key = RandomInstance.bytes(32) try { nip44v2.encrypt("a".repeat(v), key) fail("Should Throw for $v") @@ -185,5 +181,5 @@ class NIP44v2Test { } } - private fun sha256Hex(data: ByteArray) = sha256Hash(data).toHexKey() + private fun sha256Hex(data: ByteArray) = sha256(data).toHexKey() } diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip49PrivKeyEnc/NIP49Test.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip49PrivKeyEnc/NIP49Test.kt index 3985842c0c..f7876a08ee 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip49PrivKeyEnc/NIP49Test.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip49PrivKeyEnc/NIP49Test.kt @@ -22,13 +22,11 @@ package com.vitorpamplona.quartz.nip49PrivKeyEnc import androidx.test.ext.junit.runners.AndroidJUnit4 import com.vitorpamplona.quartz.nip01Core.toHexKey -import fr.acinq.secp256k1.Secp256k1 import junit.framework.TestCase.assertEquals import junit.framework.TestCase.assertNotNull import junit.framework.TestCase.fail import org.junit.Test import org.junit.runner.RunWith -import java.security.SecureRandom @RunWith(AndroidJUnit4::class) public class NIP49Test { @@ -53,8 +51,7 @@ public class NIP49Test { ) } - val random = SecureRandom() - val nip49 = Nip49(Secp256k1.get(), random) + val nip49 = Nip49() @Test fun decodeBech32() { diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip55AndroidSigner/SignStringTest.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip55AndroidSigner/SignStringTest.kt new file mode 100644 index 0000000000..bef7a3f603 --- /dev/null +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip55AndroidSigner/SignStringTest.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.nip55AndroidSigner + +import androidx.test.ext.junit.runners.AndroidJUnit4 +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 org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class SignStringTest { + @Test + fun signString() { + val random = "319cc5596fdd6cd767e5a59d976e8e059c61306af90dff1e6ee1067b3a1fdbc0".hexToByteArray() + val message = "8e58c8251bb406b6ded69e9eb14f55282a9a53bdab16fc49a3218c2ad3abc887".hexToByteArray() + val keyPair = KeyPair("a5ab474552c8f9c46c2eda5a0b68f27430ad81f96cb405e0cb4e34bf0c6494a2".hexToByteArray()) + + val signedMessage = Nip01.sign(message, keyPair.privKey!!, random).toHexKey() + val expectedValue = "0f9be7e01ba53d5ee6874b9180c7956269fda7a5be424634c3d17b5cfcea6da001be89183876415ba08b7dafa6cff4555e393dc228fb8769b384344e9a27b77c" + assertEquals(expectedValue, signedMessage) + + val message2 = "Hello" + val signedMessage2 = signString(message2, keyPair.privKey!!, random).toHexKey() + val expectedValue2 = "7ec8194a585bfb513564113b6b7bfeaafa0254c99d24eaf92280657c2291bab908b1b7bc553c83276a0254aef5041bbe6a50e93381edc4de3d859efa1c3a5a1e" + assertEquals(expectedValue2, signedMessage2) + } +} diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/utils/HexEncodingTest.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/utils/HexEncodingTest.kt index 2b6d50ce01..42686132d0 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/utils/HexEncodingTest.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/utils/HexEncodingTest.kt @@ -21,7 +21,6 @@ package com.vitorpamplona.quartz.utils import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.vitorpamplona.quartz.CryptoUtils import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue @@ -56,7 +55,7 @@ class HexEncodingTest { @Test fun testRandoms() { for (i in 0..1000) { - val bytes = CryptoUtils.privkeyCreate() + val bytes = RandomInstance.bytes(32) val hex = fr.acinq.secp256k1.Hex .encode(bytes) @@ -90,7 +89,7 @@ class HexEncodingTest { @Test fun testRandomsIsHex() { for (i in 0..10000) { - val bytes = CryptoUtils.privkeyCreate() + val bytes = RandomInstance.bytes(32) val hex = bytes.toHexString(HexFormat.Default) assertTrue(hex, Hex.isHex(hex)) val hexUpper = bytes.toHexString(HexFormat.UpperCase) @@ -102,7 +101,7 @@ class HexEncodingTest { @Test fun testRandomsUppercase() { for (i in 0..1000) { - val bytes = CryptoUtils.privkeyCreate() + val bytes = RandomInstance.bytes(32) val hex = bytes.toHexString(HexFormat.UpperCase) assertEquals( bytes.toList(), diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/CryptoUtils.kt b/quartz/src/main/java/com/vitorpamplona/quartz/CryptoUtils.kt deleted file mode 100644 index 968ba94fa2..0000000000 --- a/quartz/src/main/java/com/vitorpamplona/quartz/CryptoUtils.kt +++ /dev/null @@ -1,189 +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 - -import com.vitorpamplona.quartz.nip01Core.HexKey -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 -import com.vitorpamplona.quartz.nip49PrivKeyEnc.Nip49 -import com.vitorpamplona.quartz.utils.nextBytes -import com.vitorpamplona.quartz.utils.sha256Hash -import fr.acinq.secp256k1.Secp256k1 -import java.security.SecureRandom - -object CryptoUtils { - private val secp256k1 = Secp256k1.get() - private val random = SecureRandom() - - val nip01 = Nip01(secp256k1, random) - val nip06 = Nip06(secp256k1) - val nip04 = Nip04Encryption(secp256k1, random) - val nip44 = Nip44(secp256k1, random, nip04) - val nip49 = Nip49(secp256k1, random) - - fun clearCache() { - nip04.clearCache() - nip44.clearCache() - } - - /** Provides a 32B "private key" aka random number */ - fun privkeyCreate() = nip01.privkeyCreate() - - fun pubkeyCreate(privKey: ByteArray) = nip01.pubkeyCreate(privKey) - - fun randomInt(bound: Int) = random.nextInt(bound) - - fun random(size: Int) = random.nextBytes(size) - - fun signString( - message: String, - privKey: ByteArray, - auxrand32: ByteArray = random(32), - ): ByteArray = nip01.signString(message, privKey, auxrand32) - - fun sign( - data: ByteArray, - privKey: ByteArray, - auxrand32: ByteArray? = null, - ): ByteArray = nip01.sign(data, privKey, auxrand32) - - fun verifySignature( - signature: ByteArray, - hash: ByteArray, - pubKey: ByteArray, - ): Boolean = nip01.verify(signature, hash, pubKey) - - fun sha256(data: ByteArray): ByteArray { - // Creates a new buffer every time - return sha256Hash(data) - } - - fun decrypt( - msg: String, - privateKey: ByteArray, - pubKey: ByteArray, - ): String? = - if (Nip04Encryption.isNIP04(msg)) { - decryptNIP04(msg, privateKey, pubKey) - } else { - decryptNIP44(msg, privateKey, pubKey) - } - - /** NIP 04 Utils */ - fun encryptNIP04( - msg: String, - privateKey: ByteArray, - pubKey: ByteArray, - ): String = nip04.encrypt(msg, privateKey, pubKey) - - fun encryptNIP04( - msg: String, - sharedSecret: ByteArray, - ): Nip04Encoder = nip04.encrypt(msg, sharedSecret) - - fun decryptNIP04( - msg: String, - privateKey: ByteArray, - pubKey: ByteArray, - ): String = nip04.decrypt(msg, privateKey, pubKey) - - fun decryptNIP04( - encryptedInfo: Nip04Encoder, - privateKey: ByteArray, - pubKey: ByteArray, - ): String = nip04.decrypt(encryptedInfo, privateKey, pubKey) - - fun decryptNIP04( - msg: String, - sharedSecret: ByteArray, - ): String = nip04.decrypt(msg, sharedSecret) - - private fun decryptNIP04( - cipher: String, - nonce: String, - sharedSecret: ByteArray, - ): String = nip04.decrypt(cipher, nonce, sharedSecret) - - private fun decryptNIP04( - encryptedMsg: ByteArray, - iv: ByteArray, - sharedSecret: ByteArray, - ): String = nip04.decrypt(encryptedMsg, iv, sharedSecret) - - fun getSharedSecretNIP04( - privateKey: ByteArray, - pubKey: ByteArray, - ): ByteArray = nip04.getSharedSecret(privateKey, pubKey) - - fun computeSharedSecretNIP04( - privateKey: ByteArray, - pubKey: ByteArray, - ): ByteArray = nip04.computeSharedSecret(privateKey, pubKey) - - /** NIP 06 Utils */ - fun isValidMnemonic(mnemonic: String): Boolean = nip06.isValidMnemonic(mnemonic) - - fun privateKeyFromMnemonic( - mnemonic: String, - account: Long = 0, - ) = nip06.privateKeyFromMnemonic(mnemonic, account) - - /** NIP 44 Utils */ - fun getSharedSecretNIP44( - privateKey: ByteArray, - pubKey: ByteArray, - ): ByteArray = nip44.getSharedSecret(privateKey, pubKey) - - fun computeSharedSecretNIP44( - privateKey: ByteArray, - pubKey: ByteArray, - ): ByteArray = nip44.computeSharedSecret(privateKey, pubKey) - - fun encryptNIP44( - msg: String, - privateKey: ByteArray, - pubKey: ByteArray, - ): Nip44v2.EncryptedInfo = nip44.encrypt(msg, privateKey, pubKey) - - fun decryptNIP44( - payload: String, - privateKey: ByteArray, - pubKey: ByteArray, - ): String? = nip44.decrypt(payload, privateKey, pubKey) - - /** NIP 49 Utils */ - fun decryptNIP49( - payload: String, - password: String, - ): String? { - if (payload.isEmpty() || password.isEmpty()) return null - return nip49.decrypt(payload, password) - } - - fun encryptNIP49( - key: HexKey, - password: String, - ): String = nip49.encrypt(key, password) -} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/EventFactory.kt b/quartz/src/main/java/com/vitorpamplona/quartz/EventFactory.kt index 08b3010312..3072c450a5 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/EventFactory.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/EventFactory.kt @@ -42,7 +42,7 @@ 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.nip04Dm.messages.PrivateDmEvent import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent 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 e2e85884fe..3afaf3bc78 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/EventExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/EventExt.kt @@ -21,8 +21,8 @@ package com.vitorpamplona.quartz.nip01Core import android.util.Log -import com.vitorpamplona.quartz.CryptoUtils import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.crypto.Nip01 import com.vitorpamplona.quartz.utils.Hex fun Event.generateId(): String = EventHasher.hashId(pubKey, createdAt, kind, tags, content) @@ -34,7 +34,7 @@ fun Event.verifyId(): Boolean { fun Event.verifySignature(): Boolean { if (id.isEmpty() || sig.isEmpty()) return false - return CryptoUtils.verifySignature(Hex.decode(sig), Hex.decode(id), Hex.decode(pubKey)) + return Nip01.verify(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. */ diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/EventHasher.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/EventHasher.kt index 13a6829506..f694cd6279 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/EventHasher.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/EventHasher.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.quartz.nip01Core import com.fasterxml.jackson.databind.node.JsonNodeFactory import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper -import com.vitorpamplona.quartz.utils.sha256Hash +import com.vitorpamplona.quartz.utils.sha256 class EventHasher { companion object { @@ -61,7 +61,7 @@ class EventHasher { kind: Int, tags: Array>, content: String, - ): ByteArray = sha256Hash(makeJsonForId(pubKey, createdAt, kind, tags, content).toByteArray()) + ): ByteArray = sha256(makeJsonForId(pubKey, createdAt, kind, tags, content).toByteArray()) fun hashId( pubKey: HexKey, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/crypto/KeyPair.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/crypto/KeyPair.kt index a5ac5c4aca..4dc020aaa5 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/crypto/KeyPair.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/crypto/KeyPair.kt @@ -20,13 +20,11 @@ */ package com.vitorpamplona.quartz.nip01Core.crypto -import com.vitorpamplona.quartz.CryptoUtils import com.vitorpamplona.quartz.nip01Core.toHexKey class KeyPair( privKey: ByteArray? = null, pubKey: ByteArray? = null, - forcePubKeyCheck: Boolean = true, ) { val privKey: ByteArray? val pubKey: ByteArray @@ -35,8 +33,8 @@ class KeyPair( if (privKey == null) { if (pubKey == null) { // create new, random keys - this.privKey = CryptoUtils.privkeyCreate() - this.pubKey = CryptoUtils.pubkeyCreate(this.privKey) + this.privKey = Nip01.privKeyCreate() + this.pubKey = Nip01.pubKeyCreate(this.privKey) } else { // this is a read-only account check(pubKey.size == 32) @@ -46,12 +44,7 @@ class KeyPair( } else { // as private key is provided, ignore the public key and set keys according to private key this.privKey = privKey - - if (pubKey == null || forcePubKeyCheck) { - this.pubKey = CryptoUtils.pubkeyCreate(privKey) - } else { - this.pubKey = pubKey - } + this.pubKey = Nip01.pubKeyCreate(privKey) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/crypto/Nip01.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/crypto/Nip01.kt index 6466d3c754..d0cb443401 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/crypto/Nip01.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/crypto/Nip01.kt @@ -20,45 +20,23 @@ */ package com.vitorpamplona.quartz.nip01Core.crypto -import com.vitorpamplona.quartz.CryptoUtils -import com.vitorpamplona.quartz.utils.nextBytes -import com.vitorpamplona.quartz.utils.sha256Hash -import fr.acinq.secp256k1.Secp256k1 -import java.security.SecureRandom +import com.vitorpamplona.quartz.utils.RandomInstance +import com.vitorpamplona.quartz.utils.Secp256k1Instance -class Nip01( - val secp256k1: Secp256k1, - val random: SecureRandom, -) { - /** Provides a 32B "private key" aka random number */ - fun privkeyCreate() = random.nextBytes(32) +object Nip01 { + fun privKeyCreate() = RandomInstance.bytes(32) - fun compressedPubkeyCreate(privKey: ByteArray) = secp256k1.pubKeyCompress(secp256k1.pubkeyCreate(privKey)) - - fun pubkeyCreate(privKey: ByteArray) = compressedPubkeyCreate(privKey).copyOfRange(1, 33) + fun pubKeyCreate(privKey: ByteArray) = Secp256k1Instance.compressedPubKeyFor(privKey).copyOfRange(1, 33) fun sign( data: ByteArray, privKey: ByteArray, - nonce: ByteArray? = random.nextBytes(32), - ): ByteArray = secp256k1.signSchnorr(data, privKey, nonce) - - fun signDeterministic( - data: ByteArray, - privKey: ByteArray, - ): ByteArray = secp256k1.signSchnorr(data, privKey, null) + nonce: ByteArray? = RandomInstance.bytes(32), + ): ByteArray = Secp256k1Instance.signSchnorr(data, privKey, nonce) fun verify( signature: ByteArray, hash: ByteArray, pubKey: ByteArray, - ): Boolean = secp256k1.verifySchnorr(signature, hash, pubKey) - - fun sha256(data: ByteArray) = sha256Hash(data) - - fun signString( - message: String, - privKey: ByteArray, - nonce: ByteArray = random.nextBytes(32), - ): ByteArray = sign(CryptoUtils.sha256(message.toByteArray()), privKey, nonce) + ): Boolean = Secp256k1Instance.verifySchnorr(signature, hash, pubKey) } 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 bfda070b00..ad0f8998b4 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,7 +24,7 @@ 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.Nip04Encryption +import com.vitorpamplona.quartz.nip04Dm.crypto.EncryptedInfo import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent @@ -78,7 +78,7 @@ abstract class NostrSigner( fromPublicKey: HexKey, onReady: (String) -> Unit, ) { - if (Nip04Encryption.isNIP04(encryptedContent)) { + if (EncryptedInfo.isNIP04(encryptedContent)) { nip04Decrypt(encryptedContent, fromPublicKey, onReady) } else { nip44Decrypt(encryptedContent, fromPublicKey, onReady) 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 aabdafd1c6..b45160e435 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 @@ -21,14 +21,16 @@ package com.vitorpamplona.quartz.nip01Core.signers import android.util.Log -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.core.Event 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.nip04Dm.crypto.Nip04 +import com.vitorpamplona.quartz.nip44Encryption.Nip44 import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent import com.vitorpamplona.quartz.nip57Zaps.PrivateZapRequestBuilder @@ -69,7 +71,7 @@ class NostrSignerSync( if (keyPair.privKey == null) return null val id = EventHasher.hashIdBytes(pubKey, createdAt, kind, tags, content) - val sig = CryptoUtils.sign(id, keyPair.privKey).toHexKey() + val sig = Nip01.sign(id, keyPair.privKey).toHexKey() return EventFactory.create( id.toHexKey(), @@ -88,7 +90,7 @@ class NostrSignerSync( ): String? { if (keyPair.privKey == null) return null - return CryptoUtils.encryptNIP04( + return Nip04.encrypt( decryptedContent, keyPair.privKey, toPublicKey.hexToByteArray(), @@ -102,10 +104,7 @@ class NostrSignerSync( if (keyPair.privKey == null) return null return try { - val sharedSecret = - CryptoUtils.getSharedSecretNIP04(keyPair.privKey, fromPublicKey.hexToByteArray()) - - CryptoUtils.decryptNIP04(encryptedContent, sharedSecret) + Nip04.decrypt(encryptedContent, keyPair.privKey, fromPublicKey.hexToByteArray()) } catch (e: Exception) { Log.w("NIP04Decrypt", "Error decrypting the message ${e.message} on $encryptedContent") null @@ -118,8 +117,8 @@ class NostrSignerSync( ): String? { if (keyPair.privKey == null) return null - return CryptoUtils - .encryptNIP44( + return Nip44 + .encrypt( decryptedContent, keyPair.privKey, toPublicKey.hexToByteArray(), @@ -132,12 +131,11 @@ class NostrSignerSync( ): String? { if (keyPair.privKey == null) return null - return CryptoUtils - .decryptNIP44( - payload = encryptedContent, - privateKey = keyPair.privKey, - pubKey = fromPublicKey.hexToByteArray(), - ) + return Nip44.decrypt( + payload = encryptedContent, + privateKey = keyPair.privKey, + pubKey = fromPublicKey.hexToByteArray(), + ) } fun decryptZapEvent(event: LnZapRequestEvent): LnZapPrivateEvent? = PrivateZapRequestBuilder().decryptZapEvent(event, this) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/Nip04Encryption.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/Nip04Encryption.kt deleted file mode 100644 index f02b44eebc..0000000000 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/Nip04Encryption.kt +++ /dev/null @@ -1,130 +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.nip04Dm - -import com.vitorpamplona.quartz.nip44Encryption.SharedKeyCache -import com.vitorpamplona.quartz.utils.Hex -import fr.acinq.secp256k1.Secp256k1 -import java.security.SecureRandom -import java.util.Base64 -import javax.crypto.Cipher -import javax.crypto.spec.IvParameterSpec -import javax.crypto.spec.SecretKeySpec - -class Nip04Encryption( - val secp256k1: Secp256k1, - val random: SecureRandom, -) { - private val sharedKeyCache = SharedKeyCache() - private val h02 = Hex.decode("02") - - fun clearCache() { - sharedKeyCache.clearCache() - } - - fun encrypt( - msg: String, - privateKey: ByteArray, - pubKey: ByteArray, - ): String = encrypt(msg, getSharedSecret(privateKey, pubKey)).encodeToNIP04() - - fun encrypt( - msg: String, - sharedSecret: ByteArray, - ): 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 encryptedMsg = cipher.doFinal(msg.toByteArray()) - return Nip04Encoder(encryptedMsg, iv) - } - - fun decrypt( - msg: String, - privateKey: ByteArray, - pubKey: ByteArray, - ): String { - val sharedSecret = getSharedSecret(privateKey, pubKey) - return decrypt(msg, sharedSecret) - } - - fun decrypt( - encryptedInfo: Nip04Encoder, - privateKey: ByteArray, - pubKey: ByteArray, - ): String { - val sharedSecret = getSharedSecret(privateKey, pubKey) - return decrypt(encryptedInfo.ciphertext, encryptedInfo.nonce, sharedSecret) - } - - fun decrypt( - msg: String, - sharedSecret: ByteArray, - ): String { - val decoded = Nip04Encoder.decodeFromNIP04(msg) - check(decoded != null) { "Unable to decode msg $msg as NIP04" } - return decrypt(decoded.ciphertext, decoded.nonce, sharedSecret) - } - - fun decrypt( - cipher: String, - nonce: String, - sharedSecret: ByteArray, - ): String { - val iv = Base64.getDecoder().decode(nonce) - val encryptedMsg = Base64.getDecoder().decode(cipher) - return decrypt(encryptedMsg, iv, sharedSecret) - } - - fun decrypt( - encryptedMsg: ByteArray, - iv: ByteArray, - sharedSecret: ByteArray, - ): String { - val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") - cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(sharedSecret, "AES"), IvParameterSpec(iv)) - return String(cipher.doFinal(encryptedMsg)) - } - - fun getSharedSecret( - privateKey: ByteArray, - pubKey: ByteArray, - ): ByteArray { - val preComputed = sharedKeyCache.get(privateKey, pubKey) - if (preComputed != null) return preComputed - - val computed = computeSharedSecret(privateKey, pubKey) - sharedKeyCache.add(privateKey, pubKey, computed) - return computed - } - - /** @return 32B shared secret */ - fun computeSharedSecret( - privateKey: ByteArray, - pubKey: ByteArray, - ): ByteArray = secp256k1.pubKeyTweakMul(h02 + pubKey, privateKey).copyOfRange(1, 33) - - companion object { - fun isNIP04(encoded: String) = Nip04Encoder.isNIP04(encoded) - } -} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/crypto/AESCBC.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/crypto/AESCBC.kt new file mode 100644 index 0000000000..66e6b1bdbe --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/crypto/AESCBC.kt @@ -0,0 +1,56 @@ +/** + * 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.crypto + +import com.vitorpamplona.quartz.nip17Dm.files.encryption.NostrCipher +import com.vitorpamplona.quartz.utils.RandomInstance +import javax.crypto.Cipher +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec + +class AESCBC( + val keyBytes: ByteArray = RandomInstance.bytes(32), + val iv: ByteArray = RandomInstance.bytes(16), +) : NostrCipher { + private fun newCipher() = Cipher.getInstance("AES/CBC/PKCS5Padding") + + private fun keySpec() = SecretKeySpec(keyBytes, "AES") + + private fun param() = IvParameterSpec(iv) + + override fun name() = NAME + + override fun encrypt(bytesToEncrypt: ByteArray): ByteArray = + with(newCipher()) { + init(Cipher.ENCRYPT_MODE, keySpec(), param()) + doFinal(bytesToEncrypt) + } + + override fun decrypt(bytesToDecrypt: ByteArray): ByteArray = + with(newCipher()) { + init(Cipher.DECRYPT_MODE, keySpec(), param()) + doFinal(bytesToDecrypt) + } + + companion object { + const val NAME = "aes-cbc" + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/Nip04Encoder.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/crypto/EncryptedInfo.kt similarity index 70% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/Nip04Encoder.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/crypto/EncryptedInfo.kt index 96fbfe9221..067185f209 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/Nip04Encoder.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/crypto/EncryptedInfo.kt @@ -18,32 +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.nip04Dm +package com.vitorpamplona.quartz.nip04Dm.crypto import android.util.Log import java.util.Base64 -class Nip04Encoder( +class EncryptedInfo( val ciphertext: ByteArray, val nonce: ByteArray, ) { + fun encodeToNIP04() = encode(ciphertext, nonce) + 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") @@ -56,11 +44,20 @@ class Nip04Encoder( cleanedUp[l - 25] == '=' } - fun decodeFromNIP04(payload: String): Nip04Encoder? = + fun encode( + ciphertext: ByteArray, + nonce: ByteArray, + ): String { + val nonceB64 = Base64.getEncoder().encodeToString(nonce) + val ciphertextB64 = Base64.getEncoder().encodeToString(ciphertext) + return "$ciphertextB64?iv=$nonceB64" + } + + fun decode(payload: String): EncryptedInfo? = try { // cleaning up some bug from some client. val parts = payload.removeSuffix("-null").split("?iv=") - Nip04Encoder( + EncryptedInfo( ciphertext = Base64.getDecoder().decode(parts[0]), nonce = Base64.getDecoder().decode(parts[1]), ) @@ -69,10 +66,4 @@ class Nip04Encoder( 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/crypto/Encryption.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/crypto/Encryption.kt new file mode 100644 index 0000000000..3303c9f2aa --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/crypto/Encryption.kt @@ -0,0 +1,76 @@ +/** + * 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.crypto + +import com.vitorpamplona.quartz.utils.RandomInstance +import com.vitorpamplona.quartz.utils.Secp256k1Instance + +class Encryption { + fun encrypt( + msg: String, + privateKey: ByteArray, + pubKey: ByteArray, + ): String = encrypt(msg, computeSharedSecret(privateKey, pubKey)) + + fun decrypt( + msg: String, + privateKey: ByteArray, + pubKey: ByteArray, + ): String = decrypt(msg, computeSharedSecret(privateKey, pubKey)) + + fun encrypt( + msg: String, + sharedSecret: ByteArray, + ): String = encryptToEncoder(msg, sharedSecret).encodeToNIP04() + + fun encryptToEncoder( + msg: String, + sharedSecret: ByteArray, + ): EncryptedInfo { + val iv = RandomInstance.bytes(16) + return EncryptedInfo(AESCBC(sharedSecret, iv).encrypt(msg.toByteArray()), iv) + } + + fun decrypt( + msg: String, + sharedSecret: ByteArray, + ): String { + val decoded = EncryptedInfo.decode(msg) + check(decoded != null) { "Unable to decode msg $msg as NIP04" } + return decrypt(decoded, sharedSecret) + } + + fun decrypt( + msg: EncryptedInfo, + sharedSecret: ByteArray, + ): String = decrypt(msg.ciphertext, msg.nonce, sharedSecret) + + fun decrypt( + encryptedMsg: ByteArray, + iv: ByteArray, + sharedSecret: ByteArray, + ): String = String(AESCBC(sharedSecret, iv).decrypt(encryptedMsg)) + + fun computeSharedSecret( + privateKey: ByteArray, + pubKey: ByteArray, + ): ByteArray = Secp256k1Instance.pubKeyTweakMulCompact(pubKey, privateKey) +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/crypto/Nip04.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/crypto/Nip04.kt new file mode 100644 index 0000000000..a4d86f7f77 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/crypto/Nip04.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.nip04Dm.crypto + +import com.vitorpamplona.quartz.nip44Encryption.SharedKeyCache + +object Nip04 { + private val sharedKeyCache = SharedKeyCache() + + private val nip04 = Encryption() + + fun clearCache() { + sharedKeyCache.clearCache() + } + + fun getSharedSecret( + privateKey: ByteArray, + pubKey: ByteArray, + ): ByteArray { + val preComputed = sharedKeyCache.get(privateKey, pubKey) + if (preComputed != null) return preComputed + + val computed = nip04.computeSharedSecret(privateKey, pubKey) + sharedKeyCache.add(privateKey, pubKey, computed) + return computed + } + + fun encrypt( + msg: String, + privateKey: ByteArray, + pubKey: ByteArray, + ): String = nip04.encrypt(msg, getSharedSecret(privateKey, pubKey)) + + fun decrypt( + msg: String, + privateKey: ByteArray, + pubKey: ByteArray, + ): String = nip04.decrypt(msg, getSharedSecret(privateKey, pubKey)) + + fun decrypt( + msg: EncryptedInfo, + privateKey: ByteArray, + pubKey: ByteArray, + ): String = nip04.decrypt(msg, getSharedSecret(privateKey, pubKey)) +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/PrivateDmEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/messages/PrivateDmEvent.kt similarity index 99% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/PrivateDmEvent.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/messages/PrivateDmEvent.kt index 6f73e3195f..8b2e09b6f5 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/PrivateDmEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/messages/PrivateDmEvent.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.nip04Dm +package com.vitorpamplona.quartz.nip04Dm.messages import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.experimental.inlineMetadata.Nip54InlineMetadata diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/messages/TagArrayBuilderExt.kt similarity index 97% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/TagArrayBuilderExt.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/messages/TagArrayBuilderExt.kt index c5a018155e..2b6e87cdaf 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/TagArrayBuilderExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/messages/TagArrayBuilderExt.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.nip04Dm +package com.vitorpamplona.quartz.nip04Dm.messages import com.vitorpamplona.quartz.nip01Core.EventHintBundle import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip06KeyDerivation/Bip32SeedDerivation.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip06KeyDerivation/Bip32SeedDerivation.kt index 88fc1dcdcf..c093a2c453 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip06KeyDerivation/Bip32SeedDerivation.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip06KeyDerivation/Bip32SeedDerivation.kt @@ -20,16 +20,13 @@ */ package com.vitorpamplona.quartz.nip06KeyDerivation -import fr.acinq.secp256k1.Secp256k1 -import javax.crypto.Mac -import javax.crypto.spec.SecretKeySpec +import com.vitorpamplona.quartz.utils.Secp256k1Instance +import com.vitorpamplona.quartz.utils.hmac512 /* Simplified from: https://github.com/ACINQ/bitcoin-kmp/ */ -class Bip32SeedDerivation( - val secp256k1: Secp256k1, -) { +class Bip32SeedDerivation { class ExtendedPrivateKey( val secretkeybytes: ByteArray, val chaincode: ByteArray, @@ -46,24 +43,6 @@ class Bip32SeedDerivation( return ExtendedPrivateKey(il, ir) } - fun hmac512( - key: ByteArray, - data: ByteArray, - ): ByteArray { - val mac = Mac.getInstance("HmacSHA512") - mac.init(SecretKeySpec(key, "HmacSHA512")) - return mac.doFinal(data) - } - - fun isPrivKeyValid(il: ByteArray): Boolean = secp256k1.secKeyVerify(il) - - fun pubkeyCreateBitcoin(privKey: ByteArray) = secp256k1.pubKeyCompress(secp256k1.pubkeyCreate(privKey)) - - fun sum( - first: ByteArray, - second: ByteArray, - ): ByteArray = secp256k1.privKeyTweakAdd(first, second) - fun derivePrivateKey( parent: ExtendedPrivateKey, index: Long, @@ -73,17 +52,17 @@ class Bip32SeedDerivation( val data = arrayOf(0.toByte()).toByteArray() + parent.secretkeybytes + writeInt32BE(index.toInt()) hmac512(parent.chaincode, data) } else { - val data = pubkeyCreateBitcoin(parent.secretkeybytes) + writeInt32BE(index.toInt()) + val data = Secp256k1Instance.compressedPubKeyFor(parent.secretkeybytes) + writeInt32BE(index.toInt()) hmac512(parent.chaincode, data) } val il = i.take(32).toByteArray() val ir = i.takeLast(32).toByteArray() - require(isPrivKeyValid(il)) { "cannot generate child private key: IL is invalid" } + require(Secp256k1Instance.isPrivateKeyValid(il)) { "cannot generate child private key: IL is invalid" } - val key = sum(il, parent.secretkeybytes) + val key = Secp256k1Instance.privateKeyAdd(il, parent.secretkeybytes) - require(isPrivKeyValid(key)) { "cannot generate child private key: resulting private key is invalid" } + require(Secp256k1Instance.isPrivateKeyValid(key)) { "cannot generate child private key: resulting private key is invalid" } return ExtendedPrivateKey(key, ir) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip06KeyDerivation/Bip39Mnemonics.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip06KeyDerivation/Bip39Mnemonics.kt index 423a6e00ae..afbbaa9b13 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip06KeyDerivation/Bip39Mnemonics.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip06KeyDerivation/Bip39Mnemonics.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.quartz.nip06KeyDerivation import com.vitorpamplona.quartz.nip49PrivKeyEnc.PBKDF -import com.vitorpamplona.quartz.utils.sha256Hash +import com.vitorpamplona.quartz.utils.sha256 // CODE FROM: https://github.com/ACINQ/bitcoin-kmp/ @@ -79,7 +79,7 @@ object Bip39Mnemonics { val databits = bits.subList(0, bitlength) val checksumbits = bits.subList(bitlength, bits.size) val data = group(databits, 8).map { fromBinary(it) }.map { it.toByte() }.toByteArray() - val check = toBinary(sha256Hash(data)).take(data.size / 4) + val check = toBinary(sha256(data)).take(data.size / 4) require(check == checksumbits) { "invalid checksum" } } @@ -106,7 +106,7 @@ object Bip39Mnemonics { wordlist: Array, ): List { require(wordlist.size == 2048) { "invalid word list (size should be 2048)" } - val digits = toBinary(entropy) + toBinary(sha256Hash(entropy)).take(entropy.size / 4) + val digits = toBinary(entropy) + toBinary(sha256(entropy)).take(entropy.size / 4) return group(digits, 11).map(Bip39Mnemonics::fromBinary).map { wordlist[it] } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip06KeyDerivation/Nip06.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip06KeyDerivation/Nip06.kt index f1b4059011..6e3fe0d6ed 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip06KeyDerivation/Nip06.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip06KeyDerivation/Nip06.kt @@ -20,12 +20,8 @@ */ package com.vitorpamplona.quartz.nip06KeyDerivation -import fr.acinq.secp256k1.Secp256k1 - -class Nip06( - val secp256k1: Secp256k1, -) { - val derivation = Bip32SeedDerivation(secp256k1) +class Nip06 { + val derivation = Bip32SeedDerivation() // m/44'/1237'/'/0/0 private val nip6Base: KeyPath = diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/files/encryption/AESGCM.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/files/encryption/AESGCM.kt index 8f3c7541f9..47c3295b84 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/files/encryption/AESGCM.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/files/encryption/AESGCM.kt @@ -20,15 +20,15 @@ */ package com.vitorpamplona.quartz.nip17Dm.files.encryption -import com.vitorpamplona.quartz.CryptoUtils import com.vitorpamplona.quartz.nip01Core.toHexKey +import com.vitorpamplona.quartz.utils.RandomInstance import javax.crypto.Cipher import javax.crypto.spec.GCMParameterSpec import javax.crypto.spec.SecretKeySpec class AESGCM( - val keyBytes: ByteArray = CryptoUtils.random(32), - val nonce: ByteArray = CryptoUtils.random(16), + val keyBytes: ByteArray = RandomInstance.bytes(32), + val nonce: ByteArray = RandomInstance.bytes(16), ) : NostrCipher { private fun newCipher() = Cipher.getInstance("AES/GCM/NoPadding") 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 1222d013f5..697269977b 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.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.nip19Bech32.bech32.bechToBytes @@ -152,7 +152,7 @@ object Nip19Parser { fun decodePublicKey(key: String): ByteArray = when (val parsed = Nip19Parser.uriToRoute(key)?.entity) { - is NSec -> KeyPair(privKey = key.bechToBytes()).pubKey + is NSec -> Nip01.pubKeyCreate(parsed.hex.hexToByteArray()) is NPub -> parsed.hex.hexToByteArray() is NProfile -> parsed.hex.hexToByteArray() else -> Hex.decode(key) // crashes on purpose @@ -179,7 +179,7 @@ fun decodePrivateKeyAsHexOrNull(key: String): HexKey? = fun decodePublicKeyAsHexOrNull(key: String): HexKey? = try { when (val parsed = Nip19Parser.uriToRoute(key)?.entity) { - is NSec -> KeyPair(privKey = key.bechToBytes()).pubKey.toHexKey() + is NSec -> Nip01.pubKeyCreate(parsed.hex.hexToByteArray()).toHexKey() is NPub -> parsed.hex is NProfile -> parsed.hex is Note -> null diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/utils/RandomExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip44Encryption/EncryptedInfoString.kt similarity index 87% rename from quartz/src/main/java/com/vitorpamplona/quartz/utils/RandomExt.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip44Encryption/EncryptedInfoString.kt index 333b62a679..c17755d031 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/utils/RandomExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip44Encryption/EncryptedInfoString.kt @@ -18,8 +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.utils +package com.vitorpamplona.quartz.nip44Encryption -import java.security.SecureRandom - -fun SecureRandom.nextBytes(size: Int) = ByteArray(size).also { nextBytes(it) } +class EncryptedInfoString( + val ciphertext: String, + val nonce: String, + val v: Int, + val mac: String?, +) 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 03bb5fff37..2ecd2b31f0 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip44Encryption/Nip44.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip44Encryption/Nip44.kt @@ -22,36 +22,19 @@ package com.vitorpamplona.quartz.nip44Encryption import android.util.Log import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper -import com.vitorpamplona.quartz.nip04Dm.Nip04Encoder -import com.vitorpamplona.quartz.nip04Dm.Nip04Encryption -import fr.acinq.secp256k1.Secp256k1 -import java.security.SecureRandom +import com.vitorpamplona.quartz.nip04Dm.crypto.EncryptedInfo +import com.vitorpamplona.quartz.nip04Dm.crypto.Nip04 import java.util.Base64 -class Nip44( - secp256k1: Secp256k1, - random: SecureRandom, - val nip04: Nip04Encryption, -) { - public val v1 = Nip44v1(secp256k1, random) - public val v2 = Nip44v2(secp256k1, random) +object Nip44 { + val v1 = Nip44v1() + val v2 = Nip44v2() fun clearCache() { v1.clearCache() v2.clearCache() } - /** NIP 44v2 Utils */ - fun getSharedSecret( - privateKey: ByteArray, - pubKey: ByteArray, - ): ByteArray = v2.getConversationKey(privateKey, pubKey) - - fun computeSharedSecret( - privateKey: ByteArray, - pubKey: ByteArray, - ): ByteArray = v2.computeConversationKey(privateKey, pubKey) - fun encrypt( msg: String, privateKey: ByteArray, @@ -75,14 +58,7 @@ class Nip44( } } - class EncryptedInfoString( - val ciphertext: String, - val nonce: String, - val v: Int, - val mac: String?, - ) - - fun decryptNIP44FromJackson( + private fun decryptNIP44FromJackson( json: String, privateKey: ByteArray, pubKey: ByteArray, @@ -97,13 +73,13 @@ class Nip44( } return when (info.v) { - Nip04Encoder.V -> { + EncryptedInfo.V -> { val encryptedInfo = - Nip04Encoder( + EncryptedInfo( ciphertext = Base64.getDecoder().decode(info.ciphertext), nonce = Base64.getDecoder().decode(info.nonce), ) - nip04.decrypt(encryptedInfo, privateKey, pubKey) + Nip04.decrypt(encryptedInfo, privateKey, pubKey) } Nip44v1.EncryptedInfo.V -> { @@ -129,7 +105,7 @@ class Nip44( } } - fun decryptNIP44FromBase64( + private fun decryptNIP44FromBase64( payload: String, privateKey: ByteArray, pubKey: ByteArray, @@ -146,7 +122,7 @@ class Nip44( } return when (byteArray[0].toInt()) { - Nip04Encoder.V -> nip04.decrypt(payload, privateKey, pubKey) + EncryptedInfo.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/nip44Encryption/Nip44v1.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip44Encryption/Nip44v1.kt index 7cf47f42db..f4d30df87f 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip44Encryption/Nip44v1.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip44Encryption/Nip44v1.kt @@ -21,22 +21,14 @@ package com.vitorpamplona.quartz.nip44Encryption import android.util.Log -import com.goterl.lazysodium.SodiumAndroid -import com.goterl.lazysodium.utils.Key -import com.vitorpamplona.quartz.nip44Encryption.crypto.cryptoStreamXChaCha20Xor -import com.vitorpamplona.quartz.utils.Hex -import com.vitorpamplona.quartz.utils.sha256Hash -import fr.acinq.secp256k1.Secp256k1 -import java.security.SecureRandom +import com.vitorpamplona.quartz.utils.LibSodiumInstance +import com.vitorpamplona.quartz.utils.RandomInstance +import com.vitorpamplona.quartz.utils.Secp256k1Instance +import com.vitorpamplona.quartz.utils.sha256 import java.util.Base64 -class Nip44v1( - val secp256k1: Secp256k1, - val random: SecureRandom, -) { +class Nip44v1 { private val sharedKeyCache = SharedKeyCache() - private val h02 = Hex.decode("02") - private val libSodium = SodiumAndroid() fun clearCache() { sharedKeyCache.clearCache() @@ -55,15 +47,13 @@ class Nip44v1( msg: String, sharedSecret: ByteArray, ): EncryptedInfo { - val nonce = ByteArray(24) - random.nextBytes(nonce) + val nonce = RandomInstance.bytes(24) val cipher = - cryptoStreamXChaCha20Xor( - libSodium = libSodium, + LibSodiumInstance.cryptoStreamXChaCha20Xor( messageBytes = msg.toByteArray(), nonce = nonce, - key = Key.fromBytes(sharedSecret), + key = sharedSecret, ) return EncryptedInfo( @@ -102,12 +92,12 @@ class Nip44v1( encryptedInfo: EncryptedInfo, sharedSecret: ByteArray, ): String? = - cryptoStreamXChaCha20Xor( - libSodium = libSodium, - messageBytes = encryptedInfo.ciphertext, - nonce = encryptedInfo.nonce, - key = Key.fromBytes(sharedSecret), - )?.decodeToString() + LibSodiumInstance + .cryptoStreamXChaCha20Xor( + messageBytes = encryptedInfo.ciphertext, + nonce = encryptedInfo.nonce, + key = sharedSecret, + )?.decodeToString() fun getSharedSecret( privateKey: ByteArray, @@ -126,8 +116,8 @@ class Nip44v1( privateKey: ByteArray, pubKey: ByteArray, ): ByteArray = - sha256Hash( - secp256k1.pubKeyTweakMul(h02 + pubKey, privateKey).copyOfRange(1, 33), + sha256( + Secp256k1Instance.pubKeyTweakMulCompact(pubKey, privateKey), ) class EncryptedInfo( diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip44Encryption/Nip44v2.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip44Encryption/Nip44v2.kt index 7b3f9d49d5..5544888a1a 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip44Encryption/Nip44v2.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip44Encryption/Nip44v2.kt @@ -21,30 +21,21 @@ package com.vitorpamplona.quartz.nip44Encryption import android.util.Log -import com.goterl.lazysodium.LazySodiumAndroid -import com.goterl.lazysodium.SodiumAndroid import com.vitorpamplona.quartz.nip01Core.toHexKey import com.vitorpamplona.quartz.nip44Encryption.crypto.Hkdf -import com.vitorpamplona.quartz.utils.Hex -import fr.acinq.secp256k1.Secp256k1 +import com.vitorpamplona.quartz.utils.LibSodiumInstance +import com.vitorpamplona.quartz.utils.RandomInstance +import com.vitorpamplona.quartz.utils.Secp256k1Instance import java.nio.ByteBuffer import java.nio.ByteOrder -import java.security.SecureRandom import java.util.Base64 import kotlin.math.floor import kotlin.math.log2 -class Nip44v2( - val secp256k1: Secp256k1, - val random: SecureRandom, -) { +class Nip44v2 { private val sharedKeyCache = SharedKeyCache() - - private val libSodium = SodiumAndroid() - private val lazySodium = LazySodiumAndroid(libSodium) private val hkdf = Hkdf() - private val h02 = Hex.decode("02") private val saltPrefix = "nip44-v2".toByteArray(Charsets.UTF_8) private val hashLength = 32 @@ -65,8 +56,7 @@ class Nip44v2( plaintext: String, conversationKey: ByteArray, ): EncryptedInfo { - val nonce = ByteArray(hashLength) - random.nextBytes(nonce) + val nonce = RandomInstance.bytes(hashLength) return encryptWithNonce(plaintext, conversationKey, nonce) } @@ -78,15 +68,12 @@ class Nip44v2( val messageKeys = getMessageKeys(conversationKey, nonce) val padded = pad(plaintext) - val ciphertext = ByteArray(padded.size) - - lazySodium.cryptoStreamChaCha20IetfXor( - ciphertext, - padded, - padded.size.toLong(), - messageKeys.chachaNonce, - messageKeys.chachaKey, - ) + val ciphertext = + LibSodiumInstance.cryptoStreamChaCha20IetfXor( + padded, + messageKeys.chachaNonce, + messageKeys.chachaKey, + ) val mac = hmacAad(messageKeys.hmacKey, ciphertext, nonce) @@ -128,16 +115,12 @@ class Nip44v2( "Invalid Mac: Calculated ${calculatedMac.toHexKey()}, decoded: ${decoded.mac.toHexKey()}" } - val mLen = decoded.ciphertext.size.toLong() - val padded = ByteArray(decoded.ciphertext.size) - - lazySodium.cryptoStreamChaCha20IetfXor( - padded, - decoded.ciphertext, - mLen, - messageKey.chachaNonce, - messageKey.chachaKey, - ) + val padded = + LibSodiumInstance.cryptoStreamChaCha20IetfXor( + decoded.ciphertext, + messageKey.chachaNonce, + messageKey.chachaKey, + ) return unpad(padded) } @@ -241,7 +224,7 @@ class Nip44v2( privateKey: ByteArray, pubKey: ByteArray, ): ByteArray { - val sharedX = secp256k1.pubKeyTweakMul(h02 + pubKey, privateKey).copyOfRange(1, 33) + val sharedX = Secp256k1Instance.pubKeyTweakMulCompact(pubKey, privateKey) return hkdf.extract(sharedX, saltPrefix) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip44Encryption/crypto/SodiumUtils.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip44Encryption/crypto/SodiumUtils.kt deleted file mode 100644 index 2cc0c902f0..0000000000 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip44Encryption/crypto/SodiumUtils.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.nip44Encryption.crypto - -import com.goterl.lazysodium.SodiumAndroid -import com.goterl.lazysodium.utils.Key - -/** - * I initially extended these methods from the Sodium and SodiumAndroid classes But JNI doesn't like - * it. There is some native method overriding bug when using Kotlin extensions - */ -fun cryptoStreamXchacha20XorIc( - libSodium: SodiumAndroid, - cipher: ByteArray, - message: ByteArray, - messageLen: Long, - nonce: ByteArray, - ic: Long, - key: ByteArray, -): Int { - /** - * C++ Code: - * - * unsigned char k2[crypto_core_hchacha20_OUTPUTBYTES]; crypto_core_hchacha20(k2, n, k, NULL); - * return crypto_stream_chacha20_xor_ic( c, m, mlen, n + crypto_core_hchacha20_INPUTBYTES, ic, - * k2); - */ - val k2 = ByteArray(32) - - val nonceChaCha = nonce.drop(16).toByteArray() - assert(nonceChaCha.size == 8) - - libSodium.crypto_core_hchacha20(k2, nonce, key, null) - return libSodium.crypto_stream_chacha20_xor_ic( - cipher, - message, - messageLen, - nonceChaCha, - ic, - k2, - ) -} - -fun cryptoStreamXchacha20Xor( - libSodium: SodiumAndroid, - cipher: ByteArray, - message: ByteArray, - messageLen: Long, - nonce: ByteArray, - key: ByteArray, -): Int = cryptoStreamXchacha20XorIc(libSodium, cipher, message, messageLen, nonce, 0, key) - -fun cryptoStreamXChaCha20Xor( - libSodium: SodiumAndroid, - cipher: ByteArray, - message: ByteArray, - messageLen: Long, - nonce: ByteArray, - key: ByteArray, -): Boolean { - require(!(messageLen < 0 || messageLen > message.size)) { - "messageLen out of bounds: $messageLen" - } - return cryptoStreamXchacha20Xor( - libSodium, - cipher, - message, - messageLen, - nonce, - key, - ) == 0 -} - -fun cryptoStreamXChaCha20Xor( - libSodium: SodiumAndroid, - messageBytes: ByteArray, - nonce: ByteArray, - key: Key, -): ByteArray? { - val mLen = messageBytes.size - val cipher = ByteArray(mLen) - val successful = - cryptoStreamXChaCha20Xor(libSodium, cipher, messageBytes, mLen.toLong(), nonce, key.asBytes) - return if (successful) cipher else null -} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip49PrivKeyEnc/ByteArrayExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip49PrivKeyEnc/ByteArrayExt.kt new file mode 100644 index 0000000000..ae6a339285 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip49PrivKeyEnc/ByteArrayExt.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.nip49PrivKeyEnc + +import com.vitorpamplona.quartz.nip19Bech32.bech32.Bech32 + +fun ByteArray.toNCryptSec() = Bech32.encodeBytes(hrp = "ncryptsec", this, Bech32.Encoding.Bech32) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip49PrivKeyEnc/Nip49.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip49PrivKeyEnc/Nip49.kt index e8e7e59df5..04fc996592 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip49PrivKeyEnc/Nip49.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip49PrivKeyEnc/Nip49.kt @@ -21,24 +21,15 @@ package com.vitorpamplona.quartz.nip49PrivKeyEnc import android.util.Log -import com.goterl.lazysodium.LazySodiumAndroid -import com.goterl.lazysodium.SodiumAndroid import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.hexToByteArray import com.vitorpamplona.quartz.nip01Core.toHexKey -import com.vitorpamplona.quartz.nip19Bech32.bech32.Bech32 import com.vitorpamplona.quartz.nip19Bech32.bech32.bechToBytes -import fr.acinq.secp256k1.Secp256k1 -import java.security.SecureRandom +import com.vitorpamplona.quartz.utils.LibSodiumInstance +import com.vitorpamplona.quartz.utils.RandomInstance import java.text.Normalizer -class Nip49( - val secp256k1: Secp256k1, - val random: SecureRandom, -) { - private val libSodium = SodiumAndroid() - private val lazySodium = LazySodiumAndroid(libSodium) - +class Nip49 { fun decrypt( nCryptSec: String, password: String, @@ -56,14 +47,11 @@ class Nip49( val key = SCrypt.scrypt(normalizedPassword, encryptedInfo.salt, n, 8, 1, 32) val m = ByteArray(32) - lazySodium.cryptoAeadXChaCha20Poly1305IetfDecrypt( + LibSodiumInstance.cryptoAeadXChaCha20Poly1305IetfDecrypt( m, - longArrayOf(32L), key, encryptedInfo.encryptedKey, - encryptedInfo.encryptedKey.size.toLong(), byteArrayOf(encryptedInfo.keySecurity), - 1, encryptedInfo.nonce, key, ) @@ -87,11 +75,8 @@ class Nip49( ksb: Byte, ): String { check(secretKey.size == 32) { "invalid secret key" } - val salt = ByteArray(16) - random.nextBytes(salt) - - val nonce = ByteArray(24) - random.nextBytes(nonce) + val salt = RandomInstance.bytes(16) + val nonce = RandomInstance.bytes(24) val normalizedPassword = Normalizer.normalize(password, Normalizer.Form.NFKC).toByteArray(Charsets.UTF_8) val n = Math.pow(2.0, logn.toDouble()).toInt() @@ -102,13 +87,10 @@ class Nip49( // byte[] m, long mLen, // byte[] ad, long adLen, // byte[] nSec, byte[] nPub, byte[] k - lazySodium.cryptoAeadXChaCha20Poly1305IetfEncrypt( + LibSodiumInstance.cryptoAeadXChaCha20Poly1305IetfEncrypt( ciphertext, - longArrayOf(48), secretKey, - secretKey.size.toLong(), byteArrayOf(ksb), - 1, key, nonce, key, @@ -166,13 +148,8 @@ class Nip49( // ln(n.toDouble()).toInt().toByte(), fun encodePayload(): String = - Bech32.encodeBytes( - hrp = "ncryptsec", - byteArrayOf( - version, - logn, - ) + salt + nonce + keySecurity + encryptedKey, - Bech32.Encoding.Bech32, - ) + ( + byteArrayOf(version, logn) + salt + nonce + keySecurity + encryptedKey + ).toNCryptSec() } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/NostrSignerExternal.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/NostrSignerExternal.kt index b06db3df35..7ee5a51ad4 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/NostrSignerExternal.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/NostrSignerExternal.kt @@ -21,7 +21,6 @@ package com.vitorpamplona.quartz.nip55AndroidSigner import android.util.Log -import com.goterl.lazysodium.BuildConfig import com.vitorpamplona.quartz.EventFactory import com.vitorpamplona.quartz.nip01Core.EventHasher import com.vitorpamplona.quartz.nip01Core.HexKey @@ -87,10 +86,6 @@ class NostrSignerExternal( toPublicKey: HexKey, onReady: (String) -> Unit, ) { - if (BuildConfig.DEBUG) { - Log.d("NostrExternalSigner", "Encrypt NIP04 Event: $decryptedContent") - } - launcher.encrypt( decryptedContent, toPublicKey, @@ -104,10 +99,6 @@ class NostrSignerExternal( fromPublicKey: HexKey, onReady: (String) -> Unit, ) { - if (BuildConfig.DEBUG) { - Log.d("NostrExternalSigner", "Decrypt NIP04 Event: $encryptedContent") - } - launcher.decrypt( encryptedContent, fromPublicKey, @@ -121,10 +112,6 @@ class NostrSignerExternal( toPublicKey: HexKey, onReady: (String) -> Unit, ) { - if (BuildConfig.DEBUG) { - Log.d("NostrExternalSigner", "Encrypt NIP44 Event: $decryptedContent") - } - launcher.encrypt( decryptedContent, toPublicKey, @@ -138,10 +125,6 @@ class NostrSignerExternal( fromPublicKey: HexKey, onReady: (String) -> Unit, ) { - if (BuildConfig.DEBUG) { - Log.d("NostrExternalSigner", "Decrypt NIP44 Event: $encryptedContent") - } - launcher.decrypt( encryptedContent, fromPublicKey, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/SignString.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/SignString.kt new file mode 100644 index 0000000000..99f6d213b2 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/SignString.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.nip55AndroidSigner + +import com.vitorpamplona.quartz.nip01Core.crypto.Nip01 +import com.vitorpamplona.quartz.utils.RandomInstance +import com.vitorpamplona.quartz.utils.sha256 + +fun signString( + message: String, + privKey: ByteArray, + nonce: ByteArray = RandomInstance.bytes(32), +): ByteArray = Nip01.sign(sha256(message.toByteArray()), privKey, nonce) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/PrivateZapEncryption.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/PrivateZapEncryption.kt index 9f89384e19..61228c6756 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/PrivateZapEncryption.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/PrivateZapEncryption.kt @@ -20,8 +20,9 @@ */ package com.vitorpamplona.quartz.nip57Zaps -import com.vitorpamplona.quartz.CryptoUtils +import com.vitorpamplona.quartz.nip04Dm.crypto.Nip04 import com.vitorpamplona.quartz.nip19Bech32.bech32.Bech32 +import com.vitorpamplona.quartz.utils.sha256 import java.nio.charset.Charset import java.security.SecureRandom import javax.crypto.BadPaddingException @@ -38,7 +39,7 @@ class PrivateZapEncryption { ): ByteArray { val str = privkey + id + createdAt.toString() val strbyte = str.toByteArray(Charset.forName("utf-8")) - return CryptoUtils.sha256(strbyte) + return sha256(strbyte) } fun encryptPrivateZapMessage( @@ -46,7 +47,7 @@ class PrivateZapEncryption { privkey: ByteArray, pubkey: ByteArray, ): String { - val sharedSecret = CryptoUtils.getSharedSecretNIP04(privkey, pubkey) + val sharedSecret = Nip04.getSharedSecret(privkey, pubkey) val iv = ByteArray(16) SecureRandom().nextBytes(iv) @@ -70,7 +71,7 @@ class PrivateZapEncryption { privkey: ByteArray, pubkey: ByteArray, ): String { - val sharedSecret = CryptoUtils.getSharedSecretNIP04(privkey, pubkey) + val sharedSecret = Nip04.getSharedSecret(privkey, pubkey) if (sharedSecret.size != 16 && sharedSecret.size != 32) { throw IllegalArgumentException("Invalid shared secret size") } 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 8ee6fbf4c1..0b0a8b9f95 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/PrivateZapRequestBuilder.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/PrivateZapRequestBuilder.kt @@ -21,8 +21,8 @@ package com.vitorpamplona.quartz.nip57Zaps import android.util.Log -import com.vitorpamplona.quartz.CryptoUtils 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.signers.NostrSignerSync import com.vitorpamplona.quartz.nip01Core.toHexKey @@ -105,7 +105,7 @@ class PrivateZapRequestBuilder { try { if (altPrivateKeyToUse != null && altPubkeyToUse != null) { - val altPubKeyFromPrivate = CryptoUtils.pubkeyCreate(altPrivateKeyToUse).toHexKey() + val altPubKeyFromPrivate = Nip01.pubKeyCreate(altPrivateKeyToUse).toHexKey() if (altPubKeyFromPrivate == event.pubKey) { val result = event.getPrivateZapEvent(altPrivateKeyToUse, altPubkeyToUse) 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 index 85904bb3b7..e607ee950a 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip98HttpAuth/tags/PayloadHashTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip98HttpAuth/tags/PayloadHashTag.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.quartz.nip98HttpAuth.tags import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.toHexKey -import com.vitorpamplona.quartz.utils.sha256Hash +import com.vitorpamplona.quartz.utils.sha256 class PayloadHashTag { companion object { @@ -39,6 +39,6 @@ class PayloadHashTag { fun assemble(hash: HexKey) = arrayOf(TAG_NAME, hash) @JvmStatic - fun assemble(payload: ByteArray) = assemble(sha256Hash(payload).toHexKey()) + fun assemble(payload: ByteArray) = assemble(sha256(payload).toHexKey()) } } 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 9f1725f881..e33916bfd0 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/utils/ArrayUtils.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/utils/ArrayUtils.kt @@ -20,8 +20,6 @@ */ 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 { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/utils/Hmac512.kt b/quartz/src/main/java/com/vitorpamplona/quartz/utils/Hmac512.kt new file mode 100644 index 0000000000..c19f7cc4b6 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/utils/Hmac512.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.utils + +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec + +fun hmac512( + key: ByteArray, + data: ByteArray, +): ByteArray { + val mac = Mac.getInstance("HmacSHA512") + mac.init(SecretKeySpec(key, "HmacSHA512")) + return mac.doFinal(data) +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/utils/LibSodiumInstance.kt b/quartz/src/main/java/com/vitorpamplona/quartz/utils/LibSodiumInstance.kt new file mode 100644 index 0000000000..cdffe0a241 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/utils/LibSodiumInstance.kt @@ -0,0 +1,105 @@ +/** + * 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 com.goterl.lazysodium.LazySodiumAndroid +import com.goterl.lazysodium.SodiumAndroid + +object LibSodiumInstance { + private val libSodium = SodiumAndroid() + private val lazySodium = LazySodiumAndroid(libSodium) + + fun cryptoAeadXChaCha20Poly1305IetfDecrypt( + message: ByteArray, + nSec: ByteArray, + ciphertext: ByteArray, + ad: ByteArray, + nPub: ByteArray, + k: ByteArray, + ): Boolean = + lazySodium.cryptoAeadXChaCha20Poly1305IetfDecrypt( + message, + longArrayOf(message.size.toLong()), + nSec, + ciphertext, + ciphertext.size.toLong(), + ad, + ad.size.toLong(), + nPub, + k, + ) + + fun cryptoAeadXChaCha20Poly1305IetfEncrypt( + ciphertext: ByteArray, + message: ByteArray, + ad: ByteArray, + nSec: ByteArray, + nPub: ByteArray, + k: ByteArray, + ): Boolean = + lazySodium.cryptoAeadXChaCha20Poly1305IetfEncrypt( + ciphertext, + longArrayOf(ciphertext.size.toLong()), + message, + message.size.toLong(), + ad, + ad.size.toLong(), + nSec, + nPub, + k, + ) + + fun cryptoStreamChaCha20IetfXor( + message: ByteArray, + nonce: ByteArray?, + key: ByteArray?, + ): ByteArray { + val ciphertext = ByteArray(message.size) + lazySodium.cryptoStreamChaCha20IetfXor(ciphertext, message, message.size.toLong(), nonce, key) + return ciphertext + } + + // This function wasn't available in the bindings library. I had to move them here from C + fun cryptoStreamXChaCha20Xor( + messageBytes: ByteArray, + nonce: ByteArray, + key: ByteArray, + ): ByteArray? { + val cipher = ByteArray(messageBytes.size) + val k2 = ByteArray(32) + + val nonceChaCha = nonce.drop(16).toByteArray() + assert(nonceChaCha.size == 8) + + libSodium.crypto_core_hchacha20(k2, nonce, key, null) + val resultCode = + libSodium.crypto_stream_chacha20_xor_ic( + cipher, + messageBytes, + messageBytes.size.toLong(), + nonceChaCha, + 0, + k2, + ) + + return if (resultCode == 0) cipher else null + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/utils/RandomInstance.kt b/quartz/src/main/java/com/vitorpamplona/quartz/utils/RandomInstance.kt new file mode 100644 index 0000000000..90b0c95f3b --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/utils/RandomInstance.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.utils + +import java.security.SecureRandom + +object RandomInstance { + private val randomizer = SecureRandom() + + fun int(bound: Int) = randomizer.nextInt(bound) + + fun bytes(size: Int) = ByteArray(size).also { randomizer.nextBytes(it) } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/utils/Secp256k1Instance.kt b/quartz/src/main/java/com/vitorpamplona/quartz/utils/Secp256k1Instance.kt new file mode 100644 index 0000000000..814514cb4c --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/utils/Secp256k1Instance.kt @@ -0,0 +1,59 @@ +/** + * 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 fr.acinq.secp256k1.Secp256k1 + +object Secp256k1Instance { + private val h02 = Hex.decode("02") + private val secp256k1 = Secp256k1.get() + + fun compressedPubKeyFor(privKey: ByteArray) = secp256k1.pubKeyCompress(secp256k1.pubkeyCreate(privKey)) + + fun isPrivateKeyValid(il: ByteArray): Boolean = secp256k1.secKeyVerify(il) + + fun signSchnorr( + data: ByteArray, + privKey: ByteArray, + nonce: ByteArray? = RandomInstance.bytes(32), + ): ByteArray = secp256k1.signSchnorr(data, privKey, nonce) + + fun signSchnorr( + data: ByteArray, + privKey: ByteArray, + ): ByteArray = secp256k1.signSchnorr(data, privKey, null) + + fun verifySchnorr( + signature: ByteArray, + hash: ByteArray, + pubKey: ByteArray, + ): Boolean = secp256k1.verifySchnorr(signature, hash, pubKey) + + fun privateKeyAdd( + first: ByteArray, + second: ByteArray, + ): ByteArray = secp256k1.privKeyTweakAdd(first, second) + + fun pubKeyTweakMulCompact( + pubKey: ByteArray, + privateKey: ByteArray, + ): ByteArray = secp256k1.pubKeyTweakMul(h02 + pubKey, privateKey).copyOfRange(1, 33) +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/utils/Sha256.kt b/quartz/src/main/java/com/vitorpamplona/quartz/utils/Sha256.kt index 5eda7da51a..c33dc0b7d9 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/utils/Sha256.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/utils/Sha256.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.quartz.utils import java.security.MessageDigest -fun sha256Hash(data: ByteArray): ByteArray { +fun sha256(data: ByteArray): ByteArray { // Creates a new buffer every time return MessageDigest.getInstance("SHA-256").digest(data) } 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 8e6341c092..8193397795 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/utils/TimeUtils.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/utils/TimeUtils.kt @@ -20,8 +20,6 @@ */ package com.vitorpamplona.quartz.utils -import com.vitorpamplona.quartz.CryptoUtils - object TimeUtils { const val ONE_MINUTE = 60 const val FIVE_MINUTES = 5 * ONE_MINUTE @@ -57,5 +55,5 @@ object TimeUtils { fun oneMonthAgo() = now() - ONE_MONTH - fun randomWithTwoDays() = now() - CryptoUtils.randomInt(twoDays()) + fun randomWithTwoDays() = now() - RandomInstance.int(twoDays()) } From 9c35008fc455b54c32a744e446a64575d5e222c0 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 18 Feb 2025 09:40:17 -0500 Subject: [PATCH 26/76] Moves Sha256 to a pool of digests to double the hashing performance --- .../amethyst/service/Base64Image.kt | 2 +- .../amethyst/service/uploads/EncryptFiles.kt | 2 +- .../amethyst/service/uploads/FileHeader.kt | 2 +- .../uploads/blossom/BlossomUploader.kt | 2 +- .../ui/components/ZoomableContentView.kt | 2 +- .../quartz/benchmark/EventBenchmark.kt | 23 ++++++- .../quartz/benchmark/Sha256Benchmark.kt | 29 ++++++++- .../quartz/benchmark/SignVerifyBenchmark.kt | 2 +- .../commons/robohash/RobohashAssembler.kt | 2 +- .../quartz/nip01Core/EventHasher.kt | 52 +++++++++------- .../nip01Core/tags/addressables/Address.kt | 1 - .../nip06KeyDerivation/Bip39Mnemonics.kt | 2 +- .../quartz/nip44Encryption/Nip44v1.kt | 2 +- .../quartz/nip55AndroidSigner/SignString.kt | 2 +- .../quartz/nip57Zaps/PrivateZapEncryption.kt | 2 +- .../nip98HttpAuth/tags/PayloadHashTag.kt | 2 +- .../quartz/utils/{ => sha256}/Sha256.kt | 9 +-- .../quartz/utils/sha256/Sha256Pool.kt | 60 +++++++++++++++++++ 18 files changed, 153 insertions(+), 45 deletions(-) rename quartz/src/main/java/com/vitorpamplona/quartz/utils/{ => sha256}/Sha256.kt (83%) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/utils/sha256/Sha256Pool.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/Base64Image.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/Base64Image.kt index 41e5204c8a..5727852847 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/Base64Image.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/Base64Image.kt @@ -35,7 +35,7 @@ import coil3.request.ImageRequest import coil3.request.Options import com.vitorpamplona.amethyst.commons.richtext.RichTextParser.Companion.base64contentPattern import com.vitorpamplona.quartz.nip01Core.toHexKey -import com.vitorpamplona.quartz.utils.sha256 +import com.vitorpamplona.quartz.utils.sha256.sha256 import java.util.Base64 @Stable 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 28e40e032f..c8151e4684 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.nip01Core.toHexKey import com.vitorpamplona.quartz.nip17Dm.files.encryption.NostrCipher -import com.vitorpamplona.quartz.utils.sha256 +import com.vitorpamplona.quartz.utils.sha256.sha256 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 e22655d38e..173a46eb49 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.nip01Core.toHexKey import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag -import com.vitorpamplona.quartz.utils.sha256 +import com.vitorpamplona.quartz.utils.sha256.sha256 import kotlinx.coroutines.CancellationException import java.io.IOException diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt index a6dabb114e..ac5a33fad3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt @@ -38,7 +38,7 @@ import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.blossom.BlossomAuthorizationEvent import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.toHexKey -import com.vitorpamplona.quartz.utils.sha256 +import com.vitorpamplona.quartz.utils.sha256.sha256 import okhttp3.MediaType.Companion.toMediaType import okhttp3.Request import okhttp3.RequestBody 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 721a8ca2fd..eec79997b4 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.nip01Core.toHexKey import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag -import com.vitorpamplona.quartz.utils.sha256 +import com.vitorpamplona.quartz.utils.sha256.sha256 import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.Dispatchers 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 ef2aeca89b..160c87ae9e 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/EventBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/EventBenchmark.kt @@ -26,6 +26,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import com.vitorpamplona.quartz.EventFactory import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper import com.vitorpamplona.quartz.nip01Core.verify +import com.vitorpamplona.quartz.nip01Core.verifyId +import com.vitorpamplona.quartz.nip01Core.verifySignature import com.vitorpamplona.quartz.utils.TimeUtils import junit.framework.TestCase.assertTrue import org.junit.Rule @@ -42,6 +44,15 @@ import org.junit.runner.RunWith class EventBenchmark { @get:Rule val benchmarkRule = BenchmarkRule() + @Test + fun parseComplete() { + benchmarkRule.measureRepeated { + val tree = EventMapper.mapper.readTree(reqResponseEvent) + val event = EventMapper.fromJson(tree[2]) + assertTrue(event.verify()) + } + } + @Test fun parseREQString() { benchmarkRule.measureRepeated { EventMapper.mapper.readTree(reqResponseEvent) } @@ -54,13 +65,23 @@ class EventBenchmark { benchmarkRule.measureRepeated { EventMapper.fromJson(msg[2]) } } + @Test + fun checkId() { + val msg = EventMapper.mapper.readTree(reqResponseEvent) + val event = EventMapper.fromJson(msg[2]) + benchmarkRule.measureRepeated { + // Should pass + assertTrue(event.verifyId()) + } + } + @Test fun checkSignature() { val msg = EventMapper.mapper.readTree(reqResponseEvent) val event = EventMapper.fromJson(msg[2]) benchmarkRule.measureRepeated { // Should pass - assertTrue(event.verify()) + assertTrue(event.verifySignature()) } } diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/Sha256Benchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/Sha256Benchmark.kt index 5885c22369..7ffff23ae9 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/Sha256Benchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/Sha256Benchmark.kt @@ -25,11 +25,12 @@ import androidx.benchmark.junit4.measureRepeated import androidx.test.ext.junit.runners.AndroidJUnit4 import com.vitorpamplona.quartz.nip01Core.EventHasher import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.utils.sha256 +import com.vitorpamplona.quartz.utils.sha256.sha256 import junit.framework.TestCase.assertNotNull import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import java.security.MessageDigest /** * Benchmark, which will execute on an Android device. @@ -43,7 +44,7 @@ class Sha256Benchmark { val benchmarkRule = BenchmarkRule() @Test - fun sha256() { + fun sha256Pool() { val event = Event.fromJson(largeKind1Event) val byteArray = EventHasher.makeJsonForId(event.pubKey, event.createdAt, event.kind, event.tags, event.content).toByteArray() @@ -52,4 +53,28 @@ class Sha256Benchmark { assertNotNull(sha256(byteArray)) } } + + @Test + fun sha256NewEachTime() { + val event = Event.fromJson(largeKind1Event) + val byteArray = EventHasher.makeJsonForId(event.pubKey, event.createdAt, event.kind, event.tags, event.content).toByteArray() + + benchmarkRule.measureRepeated { + val digest = MessageDigest.getInstance("SHA-256") + assertNotNull(digest.digest(byteArray)) + } + } + + @Test + fun sha256Reuse() { + val event = Event.fromJson(largeKind1Event) + val byteArray = EventHasher.makeJsonForId(event.pubKey, event.createdAt, event.kind, event.tags, event.content).toByteArray() + + val digest = MessageDigest.getInstance("SHA-256") + + benchmarkRule.measureRepeated { + assertNotNull(digest.digest(byteArray)) + digest.reset() + } + } } diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/SignVerifyBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/SignVerifyBenchmark.kt index 01dbf62a45..29043a4d1c 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/SignVerifyBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/SignVerifyBenchmark.kt @@ -26,7 +26,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.crypto.Nip01 import com.vitorpamplona.quartz.utils.RandomInstance -import com.vitorpamplona.quartz.utils.sha256 +import com.vitorpamplona.quartz.utils.sha256.sha256 import junit.framework.TestCase.assertNotNull import org.junit.Rule import org.junit.Test diff --git a/commons/src/main/java/com/vitorpamplona/amethyst/commons/robohash/RobohashAssembler.kt b/commons/src/main/java/com/vitorpamplona/amethyst/commons/robohash/RobohashAssembler.kt index eab71fe674..9c0402dda8 100644 --- a/commons/src/main/java/com/vitorpamplona/amethyst/commons/robohash/RobohashAssembler.kt +++ b/commons/src/main/java/com/vitorpamplona/amethyst/commons/robohash/RobohashAssembler.kt @@ -86,7 +86,7 @@ import com.vitorpamplona.amethyst.commons.robohash.parts.mouth7Happy import com.vitorpamplona.amethyst.commons.robohash.parts.mouth8Buttons import com.vitorpamplona.amethyst.commons.robohash.parts.mouth9Closed import com.vitorpamplona.quartz.utils.Hex -import com.vitorpamplona.quartz.utils.sha256 +import com.vitorpamplona.quartz.utils.sha256.sha256 val Black = SolidColor(Color.Black) val Gray = SolidColor(Color(0xFF6d6e70)) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/EventHasher.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/EventHasher.kt index f694cd6279..e596772e7b 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/EventHasher.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/EventHasher.kt @@ -20,40 +20,46 @@ */ package com.vitorpamplona.quartz.nip01Core +import com.fasterxml.jackson.databind.node.ArrayNode import com.fasterxml.jackson.databind.node.JsonNodeFactory import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper -import com.vitorpamplona.quartz.utils.sha256 +import com.vitorpamplona.quartz.utils.sha256.sha256 class EventHasher { companion object { + fun makeJsonObjectForId( + pubKey: HexKey, + createdAt: Long, + kind: Int, + tags: Array>, + content: String, + ): ArrayNode { + val factory = JsonNodeFactory.instance + return factory.arrayNode(6).apply { + add(0) + add(pubKey) + add(createdAt) + add(kind) + add( + factory.arrayNode(tags.size).apply { + tags.forEach { tag -> + add( + factory.arrayNode(tag.size).apply { tag.forEach { add(it) } }, + ) + } + }, + ) + add(content) + } + } + fun makeJsonForId( pubKey: HexKey, createdAt: Long, kind: Int, tags: Array>, content: String, - ): String { - val factory = JsonNodeFactory.instance - val rawEvent = - factory.arrayNode(6).apply { - add(0) - add(pubKey) - add(createdAt) - add(kind) - add( - factory.arrayNode(tags.size).apply { - tags.forEach { tag -> - add( - factory.arrayNode(tag.size).apply { tag.forEach { add(it) } }, - ) - } - }, - ) - add(content) - } - - return EventMapper.toJson(rawEvent) - } + ): String = EventMapper.toJson(makeJsonObjectForId(pubKey, createdAt, kind, tags, content)) fun hashIdBytes( pubKey: HexKey, 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 index aa9ffe4da0..cbcbd3b75c 100644 --- 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 @@ -54,7 +54,6 @@ data class Address( 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) { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip06KeyDerivation/Bip39Mnemonics.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip06KeyDerivation/Bip39Mnemonics.kt index afbbaa9b13..11fe2f7a7e 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip06KeyDerivation/Bip39Mnemonics.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip06KeyDerivation/Bip39Mnemonics.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.quartz.nip06KeyDerivation import com.vitorpamplona.quartz.nip49PrivKeyEnc.PBKDF -import com.vitorpamplona.quartz.utils.sha256 +import com.vitorpamplona.quartz.utils.sha256.sha256 // CODE FROM: https://github.com/ACINQ/bitcoin-kmp/ diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip44Encryption/Nip44v1.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip44Encryption/Nip44v1.kt index f4d30df87f..1d13573292 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip44Encryption/Nip44v1.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip44Encryption/Nip44v1.kt @@ -24,7 +24,7 @@ import android.util.Log import com.vitorpamplona.quartz.utils.LibSodiumInstance import com.vitorpamplona.quartz.utils.RandomInstance import com.vitorpamplona.quartz.utils.Secp256k1Instance -import com.vitorpamplona.quartz.utils.sha256 +import com.vitorpamplona.quartz.utils.sha256.sha256 import java.util.Base64 class Nip44v1 { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/SignString.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/SignString.kt index 99f6d213b2..d96718340f 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/SignString.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/SignString.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.quartz.nip55AndroidSigner import com.vitorpamplona.quartz.nip01Core.crypto.Nip01 import com.vitorpamplona.quartz.utils.RandomInstance -import com.vitorpamplona.quartz.utils.sha256 +import com.vitorpamplona.quartz.utils.sha256.sha256 fun signString( message: String, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/PrivateZapEncryption.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/PrivateZapEncryption.kt index 61228c6756..1588feb579 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/PrivateZapEncryption.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/PrivateZapEncryption.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.quartz.nip57Zaps import com.vitorpamplona.quartz.nip04Dm.crypto.Nip04 import com.vitorpamplona.quartz.nip19Bech32.bech32.Bech32 -import com.vitorpamplona.quartz.utils.sha256 +import com.vitorpamplona.quartz.utils.sha256.sha256 import java.nio.charset.Charset import java.security.SecureRandom import javax.crypto.BadPaddingException 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 index e607ee950a..437624e568 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip98HttpAuth/tags/PayloadHashTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip98HttpAuth/tags/PayloadHashTag.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.quartz.nip98HttpAuth.tags import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.toHexKey -import com.vitorpamplona.quartz.utils.sha256 +import com.vitorpamplona.quartz.utils.sha256.sha256 class PayloadHashTag { companion object { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/utils/Sha256.kt b/quartz/src/main/java/com/vitorpamplona/quartz/utils/sha256/Sha256.kt similarity index 83% rename from quartz/src/main/java/com/vitorpamplona/quartz/utils/Sha256.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/utils/sha256/Sha256.kt index c33dc0b7d9..9bd9a065f7 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/utils/Sha256.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/utils/sha256/Sha256.kt @@ -18,11 +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.utils +package com.vitorpamplona.quartz.utils.sha256 -import java.security.MessageDigest +val pool = Sha256Pool(5) // max parallel operations -fun sha256(data: ByteArray): ByteArray { - // Creates a new buffer every time - return MessageDigest.getInstance("SHA-256").digest(data) -} +fun sha256(data: ByteArray) = pool.hash(data) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/utils/sha256/Sha256Pool.kt b/quartz/src/main/java/com/vitorpamplona/quartz/utils/sha256/Sha256Pool.kt new file mode 100644 index 0000000000..d94294be45 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/utils/sha256/Sha256Pool.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.utils.sha256 + +import android.util.Log +import java.security.MessageDigest +import java.util.concurrent.ArrayBlockingQueue + +class Sha256Pool( + size: Int, +) { + private val pool = ArrayBlockingQueue(size) + + private fun digest() = MessageDigest.getInstance("SHA-256") + + init { + repeat(size) { + pool.add(digest()) + } + } + + private fun acquire(): MessageDigest { + if (pool.size < 1) { + Log.w("SHA256Pool", "Pool running low in available digests") + } + return pool.take() + } + + private fun release(digest: MessageDigest) { + digest.reset() + pool.put(digest) + } + + fun hash(byteArray: ByteArray): ByteArray { + val digest = acquire() + try { + return digest.digest(byteArray) + } finally { + release(digest) + } + } +} From 8720b00d4ff98e9f701654bf9bc408282a50bdb0 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 18 Feb 2025 10:19:51 -0500 Subject: [PATCH 27/76] Separates the random benchmark from the Sign/Verify benchmarks --- .../quartz/benchmark/RandomBenchmark.kt | 50 +++++++++++++++++++ .../quartz/benchmark/SignVerifyBenchmark.kt | 8 +-- 2 files changed, 52 insertions(+), 6 deletions(-) create mode 100644 benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/RandomBenchmark.kt diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/RandomBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/RandomBenchmark.kt new file mode 100644 index 0000000000..f230500e45 --- /dev/null +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/RandomBenchmark.kt @@ -0,0 +1,50 @@ +/** + * 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.benchmark + +import androidx.benchmark.junit4.BenchmarkRule +import androidx.benchmark.junit4.measureRepeated +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.vitorpamplona.quartz.utils.RandomInstance +import junit.framework.TestCase.assertNotNull +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class RandomBenchmark { + @get:Rule val benchmarkRule = BenchmarkRule() + + @Test + fun random32Bytes() { + benchmarkRule.measureRepeated { assertNotNull(RandomInstance.bytes(32)) } + } + + @Test + fun random1000Bytes() { + benchmarkRule.measureRepeated { assertNotNull(RandomInstance.bytes(1000)) } + } + + @Test + fun randomInt() { + benchmarkRule.measureRepeated { assertNotNull(RandomInstance.int(1000)) } + } +} diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/SignVerifyBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/SignVerifyBenchmark.kt index 29043a4d1c..288995e945 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/SignVerifyBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/SignVerifyBenchmark.kt @@ -28,6 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.crypto.Nip01 import com.vitorpamplona.quartz.utils.RandomInstance import com.vitorpamplona.quartz.utils.sha256.sha256 import junit.framework.TestCase.assertNotNull +import junit.framework.TestCase.assertTrue import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -36,11 +37,6 @@ import org.junit.runner.RunWith class SignVerifyBenchmark { @get:Rule val benchmarkRule = BenchmarkRule() - @Test - fun random() { - benchmarkRule.measureRepeated { assertNotNull(RandomInstance.bytes(1000)) } - } - @Test fun sign() { val keyPair = KeyPair() @@ -56,7 +52,7 @@ class SignVerifyBenchmark { val signature = Nip01.sign(msg, keyPair.privKey!!) benchmarkRule.measureRepeated { - assertNotNull(Nip01.verify(signature, msg, keyPair.pubKey)) + assertTrue(Nip01.verify(signature, msg, keyPair.pubKey)) } } } From 9e1a2bb1e4f5c02a0fb9498145412acc8fd024c9 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 18 Feb 2025 11:29:52 -0500 Subject: [PATCH 28/76] Removes the notion of generic Address references --- .../tags/addressables/AddressReference.kt | 36 ------------------- .../nip22Comments/tags/ReplyAddressTag.kt | 8 ++--- .../nip22Comments/tags/RootAddressTag.kt | 8 ++--- 3 files changed, 6 insertions(+), 46 deletions(-) delete mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/AddressReference.kt 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 deleted file mode 100644 index c5d5046741..0000000000 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/AddressReference.kt +++ /dev/null @@ -1,36 +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.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/nip22Comments/tags/ReplyAddressTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/ReplyAddressTag.kt index b80a558bf5..e37b6afbc7 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/ReplyAddressTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/ReplyAddressTag.kt @@ -26,16 +26,14 @@ 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, + val addressId: String, + val relay: String? = null, ) { - constructor(aTagId: String, relayHint: String?) : this(AddressReference(aTagId, relayHint)) - - fun toTagArray() = assemble(ref.addressId, ref.relay) + fun toTagArray() = assemble(addressId, relay) companion object { const val TAG_NAME = "a" 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 index 2d035639f3..e4dbbc220d 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/RootAddressTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/RootAddressTag.kt @@ -26,16 +26,14 @@ 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, + val addressId: String, + val relay: String? = null, ) { - constructor(aTagId: String, relayHint: String?) : this(AddressReference(aTagId, relayHint)) - - fun toTagArray() = assemble(ref.addressId, ref.relay) + fun toTagArray() = assemble(addressId, relay) companion object { const val TAG_NAME = "A" From 2de4bb1adc7d965a9b71ac404a96511c77b8e89f Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 18 Feb 2025 11:51:16 -0500 Subject: [PATCH 29/76] Reverses ATag Generalization (It just made things more complicated) --- .../quartz/blossom/BlossomServersEvent.kt | 2 +- .../edits/PrivateOutboxRelayListEvent.kt | 2 +- .../InteractiveStoryPrologueEvent.kt | 11 +- .../InteractiveStoryReadingStateEvent.kt | 2 +- .../InteractiveStorySceneEvent.kt | 3 +- .../nip01Core/tags/addressables/ATag.kt | 48 ++++--- .../nip01Core/tags/addressables/ATagParser.kt | 132 ------------------ .../settings/ChatMessageRelayListEvent.kt | 2 +- .../quartz/nip19Bech32/entities/NAddress.kt | 4 +- .../selection/EmojiPackSelectionEvent.kt | 3 +- .../quartz/nip37Drafts/DraftEvent.kt | 4 +- .../nip50Search/SearchRelayListEvent.kt | 2 +- .../AdvertisedRelayListEvent.kt | 2 +- .../config/FileServersEvent.kt | 2 +- 14 files changed, 47 insertions(+), 172 deletions(-) delete mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/ATagParser.kt 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 2847cb4bc5..87c04a6cae 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/blossom/BlossomServersEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/blossom/BlossomServersEvent.kt @@ -55,7 +55,7 @@ class BlossomServersEvent( 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 createAddressTag(pubKey: HexKey): String = Address.assemble(KIND, pubKey, FIXED_D_TAG) fun createTagArray(servers: List): Array> = servers 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 80340b4073..40ed346ccb 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 @@ -107,7 +107,7 @@ class PrivateOutboxRelayListEvent( 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 createAddressTag(pubKey: HexKey): String = Address.assemble(KIND, pubKey, FIXED_D_TAG) fun encryptTags( privateTags: Array>? = null, 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 df6f3d15a5..50990aabf9 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 @@ -26,6 +26,7 @@ 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.addressables.Address import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag import com.vitorpamplona.quartz.nip22Comments.RootScope import com.vitorpamplona.quartz.utils.TimeUtils @@ -43,16 +44,16 @@ class InteractiveStoryPrologueEvent( const val KIND = 30296 const val ALT = "The prologue of an interactive story called " + fun createAddress( + pubKey: HexKey, + dtag: String, + ): String = Address.assemble(KIND, pubKey, dtag) + fun createAddressATag( pubKey: HexKey, dtag: String, ): ATag = ATag(KIND, pubKey, dtag, null) - fun createAddressTag( - pubKey: HexKey, - dtag: String, - ): String = ATag.assembleATagId(KIND, pubKey, dtag) - fun build( baseId: String, title: String, 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 1ee7fab68c..f409a8c49a 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 @@ -78,7 +78,7 @@ class InteractiveStoryReadingStateEvent( fun createAddressTag( pubKey: HexKey, dtag: String, - ): String = ATag.assembleATagId(KIND, pubKey, dtag) + ): String = Address.assemble(KIND, pubKey, dtag) fun update( base: InteractiveStoryReadingStateEvent, 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 4b3a8cff89..f647d793d4 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 @@ -25,6 +25,7 @@ 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.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag import com.vitorpamplona.quartz.nip22Comments.RootScope import com.vitorpamplona.quartz.utils.TimeUtils @@ -50,7 +51,7 @@ class InteractiveStorySceneEvent( fun createAddressTag( pubKey: HexKey, dtag: String, - ): String = ATag.assembleATagId(KIND, pubKey, dtag) + ): String = Address.assemble(KIND, pubKey, dtag) fun build( baseId: String, 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 10c9e67505..df4df4d191 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 @@ -25,6 +25,7 @@ import com.vitorpamplona.quartz.nip01Core.HexKey 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.arrayOfNotNull import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.pointerSizeInBytes import com.vitorpamplona.quartz.utils.removeTrailingNullsAndEmptyOthers @@ -45,7 +46,7 @@ data class ATag( dTag.bytesUsedInMemory() + (relay?.bytesUsedInMemory() ?: 0) - fun toTag() = assembleATagId(kind, pubKeyHex, dTag) + fun toTag() = Address.assemble(kind, pubKeyHex, dTag) fun toATagArray() = removeTrailingNullsAndEmptyOthers(TAG_NAME, toTag(), relay) @@ -65,68 +66,73 @@ data class ATag( fun isTagged( tag: Array, addressId: String, - ) = ATagParser.isTagged(tag, TAG_NAME, addressId) + ) = tag.size >= TAG_SIZE && tag[0] == TAG_NAME && tag[1] == addressId @JvmStatic fun isTagged( tag: Array, address: ATag, - ) = ATagParser.isTagged(tag, TAG_NAME, address.toTag()) + ) = tag.size >= TAG_SIZE && tag[0] == TAG_NAME && tag[1] == address.toTag() @JvmStatic fun isIn( tag: Array, addressIds: Set, - ) = ATagParser.isIn(tag, TAG_NAME, addressIds) + ) = tag.size >= TAG_SIZE && tag[0] == TAG_NAME && tag[1] in addressIds @JvmStatic fun isTaggedWithKind( tag: Array, kind: String, - ) = ATagParser.isTaggedWithKind(tag, TAG_NAME, kind) - - @JvmStatic - fun assembleATagId( - kind: Int, - pubKey: HexKey, - dTag: String, - ) = ATagParser.assembleATagId(kind, pubKey, dTag) + ) = tag.size >= TAG_SIZE && tag[0] == TAG_NAME && Address.isOfKind(tag[1], kind) @JvmStatic fun parseIfOfKind( tag: Array, kind: String, - ) = ATagParser.parseIfOfKind(tag, TAG_NAME, kind) + ) = if (isTaggedWithKind(tag, kind)) parse(tag[1], tag.getOrNull(2)) else null @JvmStatic fun parseIfIsIn( tag: Array, addresses: Set, - ) = ATagParser.parseIfIsIn(tag, TAG_NAME, addresses) + ) = if (isIn(tag, addresses)) parse(tag[1], tag.getOrNull(2)) else null @JvmStatic fun parse( aTagId: String, relay: String?, - ) = ATagParser.parse(aTagId, relay) + ) = Address.parse(aTagId)?.let { ATag(it.kind, it.pubKeyHex, it.dTag, relay) } @JvmStatic - fun parse(tag: Array) = ATagParser.parse(TAG_NAME, tag) + fun parse(tag: Array): ATag? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return parse(tag[1], tag.getOrNull(2)) + } @JvmStatic - fun parseValidAddress(tag: Array) = ATagParser.parseValidAddress(TAG_NAME, tag) + 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) = ATagParser.parseAddress(TAG_NAME, tag) + fun parseAddress(tag: Array): Address? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return Address.parse(tag[1]) + } @JvmStatic - fun parseAddressId(tag: Array) = ATagParser.parseAddressId(TAG_NAME, tag) + fun parseAddressId(tag: Array): String? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + return tag[1] + } @JvmStatic fun assemble( aTagId: HexKey, relay: String?, - ) = ATagParser.assemble(TAG_NAME, aTagId, relay) + ) = arrayOfNotNull(TAG_NAME, aTagId, relay) @JvmStatic fun assemble( @@ -134,6 +140,6 @@ data class ATag( pubKey: String, dTag: String, relay: String?, - ) = ATagParser.assemble(TAG_NAME, kind, pubKey, dTag, relay) + ) = assemble(Address.assemble(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 deleted file mode 100644 index 2523600078..0000000000 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/ATagParser.kt +++ /dev/null @@ -1,132 +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.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, - ): Address? { - if (tag.isNotName(tagName, TAG_SIZE)) return null - return Address.parse(tag[1]) - } - - @JvmStatic - fun parseAddressId( - 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/nip17Dm/settings/ChatMessageRelayListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/settings/ChatMessageRelayListEvent.kt index c53ccaca25..c48500e6f3 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/settings/ChatMessageRelayListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/settings/ChatMessageRelayListEvent.kt @@ -55,7 +55,7 @@ class ChatMessageRelayListEvent( 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 createAddressTag(pubKey: HexKey): String = Address.assemble(KIND, pubKey, FIXED_D_TAG) fun createTagArray(relays: List): Array> = relays diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/entities/NAddress.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/entities/NAddress.kt index 83720e5b28..0e231211be 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/entities/NAddress.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/entities/NAddress.kt @@ -26,7 +26,7 @@ import addString import addStringIfNotNull import android.util.Log import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip19Bech32.TlvTypes import com.vitorpamplona.quartz.nip19Bech32.bech32.bechToBytes import com.vitorpamplona.quartz.nip19Bech32.tlv.Tlv @@ -40,7 +40,7 @@ data class NAddress( val dTag: String, val relay: List, ) : Entity { - fun aTag(): String = ATag.assembleATagId(kind, author, dTag) + fun aTag(): String = Address.assemble(kind, author, dTag) companion object { fun parse(naddr: String): NAddress? { 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 index b55cb6eb8e..0d5a5a6bcb 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip30CustomEmoji/selection/EmojiPackSelectionEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip30CustomEmoji/selection/EmojiPackSelectionEvent.kt @@ -27,7 +27,6 @@ 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.Address import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedATags import com.vitorpamplona.quartz.nip30CustomEmoji.pack.EmojiPackEvent @@ -52,7 +51,7 @@ class EmojiPackSelectionEvent( fun createAddress(pubKey: HexKey) = Address(KIND, pubKey, FIXED_D_TAG) - fun createAddressTag(pubKey: HexKey) = ATag.assembleATagId(KIND, pubKey, FIXED_D_TAG) + fun createAddressTag(pubKey: HexKey) = Address.assemble(KIND, pubKey, FIXED_D_TAG) fun add( currentSelection: EmojiPackSelectionEvent, 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 c75bc31405..11156dafce 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip37Drafts/DraftEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip37Drafts/DraftEvent.kt @@ -27,7 +27,7 @@ import com.vitorpamplona.quartz.nip01Core.HexKey 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.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent @@ -125,7 +125,7 @@ class DraftEvent( fun createAddressTag( pubKey: HexKey, dTag: String, - ): String = ATag.assembleATagId(KIND, pubKey, dTag) + ): String = Address.assemble(KIND, pubKey, dTag) fun create( dTag: String, 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 5f78cb5a08..088057ef75 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip50Search/SearchRelayListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip50Search/SearchRelayListEvent.kt @@ -55,7 +55,7 @@ class SearchRelayListEvent( 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 createAddressTag(pubKey: HexKey): String = Address.assemble(KIND, pubKey, FIXED_D_TAG) fun createTagArray(relays: List): Array> = relays 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 e0b918f447..d38259ecff 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip65RelayList/AdvertisedRelayListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip65RelayList/AdvertisedRelayListEvent.kt @@ -90,7 +90,7 @@ class AdvertisedRelayListEvent( 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 createAddressTag(pubKey: HexKey): String = Address.assemble(KIND, pubKey, FIXED_D_TAG) fun updateRelayList( earlierVersion: AdvertisedRelayListEvent, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip96FileStorage/config/FileServersEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip96FileStorage/config/FileServersEvent.kt index ffaefe6590..0952f016f7 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip96FileStorage/config/FileServersEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip96FileStorage/config/FileServersEvent.kt @@ -50,7 +50,7 @@ class FileServersEvent( 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 createAddressTag(pubKey: HexKey): String = Address.assemble(KIND, pubKey, FIXED_D_TAG) fun replaceServers( earlierVersion: FileServersEvent, From 4fcf962c716d66c4c2410edf063bbd37b1b351a8 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 18 Feb 2025 12:00:22 -0500 Subject: [PATCH 30/76] Fixes r-tag migration --- .../nip01Core/tags/references/EventExt.kt | 11 ++------- .../nip01Core/tags/references/ReferenceTag.kt | 20 ++++++++++++++++ .../nip01Core/tags/references/TagArrayExt.kt | 23 ++++--------------- 3 files changed, 27 insertions(+), 27 deletions(-) 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 index a14d9296e8..679879c915 100644 --- 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 @@ -20,19 +20,12 @@ */ 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.isTaggedReference(reference: String) = tags.isTaggedReference(reference) -fun Event.isTaggedReferences(hashtags: Set) = tags.isTaggedReferences(hashtags) - -fun Event.firstIsTaggedReferences(hashtags: Set) = tags.firstIsTaggedReferences(hashtags) +fun Event.isTaggedReferences(references: Set) = tags.isTaggedReferences(references) 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 index a601678077..8c07af9ffa 100644 --- 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 @@ -27,14 +27,34 @@ class ReferenceTag { const val TAG_NAME = "r" const val TAG_SIZE = 2 + @JvmStatic + fun isTagged( + tag: Array, + reference: String, + ): Boolean = tag.size >= 2 && tag[0] == TAG_NAME && tag[1] == reference + + @JvmStatic + fun isIn( + tag: Array, + references: Set, + ): Boolean = tag.size >= 2 && tag[0] == TAG_NAME && tag[1] in references + + @JvmStatic + fun hasReference(tag: Array): Boolean { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return false + return tag[1].isNotEmpty() + } + @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, HttpUrlFormatter.normalize(url)) + @JvmStatic 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/nip01Core/tags/references/TagArrayExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/references/TagArrayExt.kt index c7c74d35a2..4a3fb5427f 100644 --- 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 @@ -20,26 +20,13 @@ */ 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 +import com.vitorpamplona.quartz.nip01Core.core.any -fun TagArray.forEachReference(onEach: (eventId: HexKey) -> Unit) = this.forEachTagged(ReferenceTag.TAG_NAME, onEach) +fun TagArray.hasReferences() = this.any(ReferenceTag::hasReference) -fun TagArray.anyReference(onEach: (str: String) -> Boolean) = this.anyTagged(ReferenceTag.TAG_NAME, onEach) +fun TagArray.references() = this.mapNotNull(ReferenceTag::parse) -fun TagArray.hasReferences() = this.hasTagWithContent(ReferenceTag.TAG_NAME) +fun TagArray.isTaggedReference(reference: String) = this.any(ReferenceTag::isTagged, reference) -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) +fun TagArray.isTaggedReferences(references: Set) = this.any(ReferenceTag::isIn, references) From bcb3838e85de7790728c2e688fae3f4473b621ea Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 18 Feb 2025 12:04:39 -0500 Subject: [PATCH 31/76] Fixes isTagged for events --- .../com/vitorpamplona/quartz/nip01Core/tags/events/ETag.kt | 6 ++++++ .../quartz/nip01Core/tags/events/TagArrayExt.kt | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) 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 c4c0b08fd2..0fb707e07a 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 @@ -60,6 +60,12 @@ data class ETag( @JvmStatic fun isTagged(tag: Array) = tag.size >= TAG_SIZE && tag[0] == TAG_NAME && tag[1].isNotEmpty() + @JvmStatic + fun isTagged( + tag: Array, + eventId: HexKey, + ) = tag.size >= TAG_SIZE && tag[0] == TAG_NAME && tag[1] == eventId + @JvmStatic fun parse(tag: Array): ETag? { if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null 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 b0ed27872a..f4aa37bfde 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,6 +22,7 @@ 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.any import com.vitorpamplona.quartz.nip01Core.core.forEachTagged import com.vitorpamplona.quartz.nip01Core.core.isTagged import com.vitorpamplona.quartz.nip01Core.core.mapValueTagged @@ -36,4 +37,4 @@ fun TagArray.taggedEventIds() = this.mapNotNull(ETag::parseId) fun TagArray.firstTaggedEvent() = this.firstNotNullOfOrNull(ETag::parse) -fun TagArray.isTaggedEvent(idHex: String) = this.isTagged(ETag.TAG_NAME, idHex) +fun TagArray.isTaggedEvent(idHex: String) = this.any(ETag::isTagged, idHex) From 9689acb5f3d43b515f3178da7b5a355a919e14fd Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 18 Feb 2025 17:47:34 -0500 Subject: [PATCH 32/76] Deletes experimental serializer on NIP-01 Moves classes to the appropriate packages on NIP-01 Adds a PoW miner to quartz. Adds a PoW commit evaluator as a bytearray of the ID --- .../amethyst/ImageUploadTesting.kt | 2 +- .../amethyst/UrlUserTagTransformationTest.kt | 2 +- .../amethyst/LocalPreferences.kt | 6 +- .../vitorpamplona/amethyst/ServiceManager.kt | 2 +- .../vitorpamplona/amethyst/model/Account.kt | 4 +- .../amethyst/model/AccountSettings.kt | 4 +- .../amethyst/model/AntiSpamFilter.kt | 2 +- .../vitorpamplona/amethyst/model/Channel.kt | 3 +- .../vitorpamplona/amethyst/model/Chatroom.kt | 2 +- .../amethyst/model/LocalCache.kt | 2 +- .../com/vitorpamplona/amethyst/model/Note.kt | 3 +- .../amethyst/model/ParticipantListBuilder.kt | 2 +- .../amethyst/model/ThreadLevelCalculator.kt | 2 +- .../com/vitorpamplona/amethyst/model/User.kt | 2 +- .../amethyst/service/Base64Image.kt | 2 +- .../amethyst/service/CashuProcessor.kt | 2 +- .../service/NostrAccountDataSource.kt | 2 +- .../NostrSearchEventOrUserDataSource.kt | 4 +- .../amethyst/service/uploads/EncryptFiles.kt | 2 +- .../amethyst/service/uploads/FileHeader.kt | 2 +- .../service/uploads/MediaUploadResult.kt | 2 +- .../uploads/blossom/BlossomUploader.kt | 4 +- .../amethyst/ui/actions/NewMessageTagger.kt | 2 +- .../amethyst/ui/actions/NewPostViewModel.kt | 2 +- .../ui/actions/UrlUserTagTransformation.kt | 2 +- .../ui/actions/relays/BasicRelaySetupInfo.kt | 2 +- .../amethyst/ui/components/ClickableRoute.kt | 2 +- .../ui/components/ZoomableContentView.kt | 2 +- .../amethyst/ui/dal/CommunityFeedFilter.kt | 2 +- .../amethyst/ui/dal/NotificationFeedFilter.kt | 2 +- .../amethyst/ui/navigation/DrawerContent.kt | 2 +- .../amethyst/ui/navigation/RouteMaker.kt | 2 +- .../amethyst/ui/note/ChannelCardCompose.kt | 2 +- .../amethyst/ui/note/PubKeyFormatter.kt | 4 +- .../amethyst/ui/note/UpdateZapAmountDialog.kt | 2 +- .../note/elements/AddInboxRelayForDMCard.kt | 2 +- .../elements/AddInboxRelayForSearchCard.kt | 2 +- .../ui/note/types/InteractiveStory.kt | 2 +- .../ui/screen/AccountStateViewModel.kt | 4 +- .../ui/screen/loggedIn/AccountBackupDialog.kt | 2 +- .../ui/screen/loggedIn/AccountViewModel.kt | 2 +- .../ui/screen/loggedIn/LoadRedirectScreen.kt | 2 +- .../chatrooms/ChatroomHeaderCompose.kt | 2 +- .../notifications/NotificationSummaryState.kt | 2 +- .../zaps/UserProfileZapsFeedFilterTest.kt | 2 +- .../ui/actions/NewPostViewModelTest.kt | 2 +- .../filters/SinceAuthorPerRelayFilter.kt | 2 +- .../quartz/benchmark/CacheBenchmark.kt | 2 +- .../benchmark/EventCmdSerializerBenchmark.kt | 33 +-- .../benchmark/GiftWrapReceivingBenchmark.kt | 2 +- .../quartz/benchmark/LargeCacheBenchmark.kt | 2 +- .../quartz/benchmark/PoWBenchmark.kt | 97 ++++++ .../quartz/benchmark/Sha256Benchmark.kt | 2 +- .../amethyst/commons/data/DeletionIndex.kt | 2 +- .../vitorpamplona/quartz/bloom/BloomFilter.kt | 10 +- .../quartz/nip01Core/Nip01Test.kt | 4 +- .../nip01Core/crypto/Nip01CryptoTest.kt | 4 +- .../quartz/nip04Dm/EncryptionTest.kt | 4 +- .../Bip32SeedDerivationTest.kt | 2 +- .../nip06KeyDerivation/Bip39MnemonicsTest.kt | 2 +- .../quartz/nip06KeyDerivation/Nip06Test.kt | 2 +- .../quartz/nip13Pow/PoWMinerTest.kt | 68 +++++ .../quartz/nip13Pow/PoWRankEvaluatorTest.kt | 73 +++++ .../quartz/nip17Dm/AESGCMTest.kt | 2 +- .../quartz/nip19Bech32/NIP19EmbedTests.kt | 2 +- .../quartz/nip44Encryption/Nip44v1Test.kt | 4 +- .../quartz/nip44Encryption/Nip44v2Test.kt | 6 +- .../quartz/nip49PrivKeyEnc/NIP49Test.kt | 2 +- .../nip55AndroidSigner/SignStringTest.kt | 4 +- .../quartz/nip57Zaps/PrivateZapTests.kt | 2 +- .../nip59GiftWraps/GiftWrapEventTest.kt | 4 +- .../com/vitorpamplona/quartz/EventFactory.kt | 4 +- .../blossom/BlossomAuthorizationEvent.kt | 2 +- .../quartz/blossom/BlossomServersEvent.kt | 2 +- .../audio/header/AudioHeaderEvent.kt | 2 +- .../audio/track/AudioTrackEvent.kt | 2 +- .../audio/track/tags/ParticipantTag.kt | 2 +- .../edits/PrivateOutboxRelayListEvent.kt | 2 +- .../edits/TextNoteModificationEvent.kt | 2 +- .../forks/BaseThreadedEventExt.kt | 2 +- .../InteractiveStoryBaseEvent.kt | 2 +- .../InteractiveStoryPrologueEvent.kt | 2 +- .../InteractiveStoryReadingStateEvent.kt | 2 +- .../InteractiveStorySceneEvent.kt | 2 +- .../interactiveStories/tags/RootSceneTag.kt | 2 +- .../experimental/medical/FhirResourceEvent.kt | 2 +- .../nip95/data/FileStorageEvent.kt | 2 +- .../nip95/header/FileStorageHeaderEvent.kt | 2 +- .../nip95/header/TagArrayBuilderExt.kt | 2 +- .../quartz/experimental/nns/NNSEvent.kt | 2 +- .../profileGallery/GalleryListEvent.kt | 2 +- .../ProfileGalleryEntryEvent.kt | 2 +- .../profileGallery/TagArrayBuilderExt.kt | 2 +- .../RelationshipStatusEvent.kt | 2 +- .../experimental/zapPolls/PollNoteEvent.kt | 2 +- .../quartz/nip01Core/EventExt.kt | 1 + .../nip01Core/core/BaseAddressableEvent.kt | 1 - .../nip01Core/core/BaseReplaceableEvent.kt | 1 - .../quartz/nip01Core/core/Event.kt | 1 - .../quartz/nip01Core/{ => core}/HexKey.kt | 2 +- .../quartz/nip01Core/core/TagArray.kt | 2 - .../nip01Core/{ => crypto}/EventHasher.kt | 6 +- .../quartz/nip01Core/crypto/KeyPair.kt | 2 +- .../nip01Core/experimental/Nip01Serializer.kt | 277 ------------------ .../jackson/EventManualSerializer.kt | 2 +- .../nip01Core/metadata/MetadataEvent.kt | 2 +- .../nip01Core/relay/SimpleClientRelay.kt | 2 +- .../relay/commands/toClient/OkMessage.kt | 2 +- .../quartz/nip01Core/signers/NostrSigner.kt | 4 +- .../nip01Core/signers/NostrSignerInternal.kt | 4 +- .../nip01Core/signers/NostrSignerSync.kt | 10 +- .../nip01Core/tags/addressables/ATag.kt | 2 +- .../nip01Core/tags/addressables/Address.kt | 2 +- .../quartz/nip01Core/tags/events/ETag.kt | 2 +- .../quartz/nip01Core/tags/events/EventExt.kt | 2 +- .../nip01Core/tags/events/EventReference.kt | 2 +- .../nip01Core/tags/events/GenericETag.kt | 2 +- .../nip01Core/tags/events/TagArrayExt.kt | 3 +- .../nip01Core/tags/hashtags/EventExt.kt | 2 +- .../nip01Core/tags/hashtags/TagArrayExt.kt | 2 +- .../quartz/nip01Core/tags/kinds/EventExt.kt | 2 +- .../nip01Core/tags/kinds/TagArrayExt.kt | 2 +- .../quartz/nip01Core/tags/people/PTag.kt | 4 +- .../tags/people/PubKeyReferenceTag.kt | 2 +- .../tags/people/TagArrayBuilderExt.kt | 2 +- .../nip02FollowList/ContactListEvent.kt | 2 +- .../quartz/nip02FollowList/ContactTag.kt | 4 +- .../quartz/nip03Timestamp/OtsEvent.kt | 4 +- .../nip03Timestamp/VerificationStateCache.kt | 2 +- .../quartz/nip04Dm/messages/PrivateDmEvent.kt | 2 +- .../quartz/nip09Deletions/DeletionEvent.kt | 2 +- .../quartz/nip10Notes/BaseThreadedEvent.kt | 2 +- .../quartz/nip10Notes/TextNoteEvent.kt | 2 +- .../quartz/nip10Notes/tags/MarkedETag.kt | 2 +- .../vitorpamplona/quartz/nip13Pow/EventExt.kt | 9 +- .../quartz/nip13Pow/TagArrayBuilderExt.kt | 1 + .../quartz/nip13Pow/TagArrayExt.kt | 3 + .../quartz/nip13Pow/miner/ByteArrayExt.kt} | 46 +-- .../quartz/nip13Pow/miner/MiningBuffer.kt | 29 ++ .../quartz/nip13Pow/miner/PoWMiner.kt | 106 +++++++ .../PoWRankEvaluator.kt} | 46 ++- .../quartz/nip13Pow/{ => tags}/PoWTag.kt | 2 +- .../quartz/nip17Dm/NIP17Factory.kt | 2 +- .../quartz/nip17Dm/base/BaseDMGroupEvent.kt | 2 +- .../quartz/nip17Dm/base/ChatroomKey.kt | 2 +- .../quartz/nip17Dm/base/ChatroomKeyable.kt | 2 +- .../quartz/nip17Dm/base/NIP17Group.kt | 2 +- .../ChatMessageEncryptedFileHeaderEvent.kt | 2 +- .../nip17Dm/files/TagArrayBuilderExt.kt | 2 +- .../quartz/nip17Dm/files/encryption/AESGCM.kt | 2 +- .../nip17Dm/files/tags/EncryptionKey.kt | 4 +- .../nip17Dm/files/tags/EncryptionNonce.kt | 4 +- .../nip17Dm/messages/ChatMessageEvent.kt | 2 +- .../settings/ChatMessageRelayListEvent.kt | 2 +- .../quartz/nip18Reposts/GenericRepostEvent.kt | 2 +- .../quartz/nip18Reposts/RepostEvent.kt | 2 +- .../quartz/nip18Reposts/quotes/EventExt.kt | 2 +- .../nip18Reposts/quotes/QAddressableTag.kt | 2 +- .../quartz/nip18Reposts/quotes/QEventTag.kt | 2 +- .../quartz/nip18Reposts/quotes/TagArrayExt.kt | 2 +- .../quartz/nip19Bech32/Nip19Parser.kt | 6 +- .../quartz/nip19Bech32/TlvBuilderExt.kt | 2 +- .../quartz/nip19Bech32/entities/NPub.kt | 6 +- .../quartz/nip19Bech32/entities/NSec.kt | 2 +- .../quartz/nip19Bech32/entities/Note.kt | 6 +- .../quartz/nip19Bech32/tlv/Tlv.kt | 2 +- .../quartz/nip19Bech32/tlv/TlvBuilder.kt | 4 +- .../quartz/nip22Comments/CommentEvent.kt | 2 +- .../nip22Comments/TagArrayBuilderExt.kt | 2 +- .../nip22Comments/tags/ReplyAddressTag.kt | 2 +- .../nip22Comments/tags/ReplyAuthorTag.kt | 2 +- .../nip22Comments/tags/ReplyEventTag.kt | 2 +- .../nip22Comments/tags/RootAddressTag.kt | 2 +- .../nip22Comments/tags/RootAuthorTag.kt | 2 +- .../quartz/nip22Comments/tags/RootEventTag.kt | 2 +- .../nip23LongContent/LongTextNoteEvent.kt | 2 +- .../quartz/nip25Reactions/ReactionEvent.kt | 2 +- .../nip28PublicChat/ChannelListEvent.kt | 2 +- .../admin/ChannelCreateEvent.kt | 2 +- .../admin/ChannelHideMessageEvent.kt | 2 +- .../admin/ChannelMetadataEvent.kt | 2 +- .../admin/ChannelMuteUserEvent.kt | 2 +- .../base/BasePublicChatEvent.kt | 2 +- .../message/ChannelMessageEvent.kt | 2 +- .../nip30CustomEmoji/pack/EmojiPackEvent.kt | 2 +- .../selection/EmojiPackSelectionEvent.kt | 2 +- .../quartz/nip34Git/issue/GitIssueEvent.kt | 2 +- .../quartz/nip34Git/patch/GitPatchEvent.kt | 2 +- .../quartz/nip34Git/reply/GitReplyEvent.kt | 2 +- .../nip34Git/repository/GitRepositoryEvent.kt | 2 +- .../nip35Torrents/TorrentCommentEvent.kt | 2 +- .../quartz/nip35Torrents/TorrentEvent.kt | 2 +- .../quartz/nip37Drafts/DraftEvent.kt | 2 +- .../quartz/nip38UserStatus/StatusEvent.kt | 2 +- .../quartz/nip42RelayAuth/RelayAuthEvent.kt | 2 +- .../quartz/nip44Encryption/Nip44v2.kt | 2 +- .../nip46RemoteSigner/BunkerRequestConnect.kt | 2 +- .../BunkerRequestNip04Decrypt.kt | 2 +- .../BunkerRequestNip04Encrypt.kt | 2 +- .../BunkerRequestNip44Decrypt.kt | 2 +- .../BunkerRequestNip44Encrypt.kt | 2 +- .../BunkerResponsePublicKey.kt | 2 +- .../nip46RemoteSigner/NostrConnectEvent.kt | 2 +- .../LnZapPaymentRequestEvent.kt | 2 +- .../LnZapPaymentResponseEvent.kt | 2 +- .../nip47WalletConnect/Nip47WalletConnect.kt | 4 +- .../quartz/nip49PrivKeyEnc/Nip49.kt | 6 +- .../nip50Search/SearchRelayListEvent.kt | 2 +- .../quartz/nip51Lists/BookmarkListEvent.kt | 2 +- .../quartz/nip51Lists/GeneralListEvent.kt | 2 +- .../quartz/nip51Lists/MuteListEvent.kt | 2 +- .../quartz/nip51Lists/PeopleListEvent.kt | 2 +- .../quartz/nip51Lists/PinListEvent.kt | 2 +- .../quartz/nip51Lists/PrivateTagArrayEvent.kt | 2 +- .../quartz/nip51Lists/RelaySetEvent.kt | 2 +- .../nip52Calendar/CalendarDateSlotEvent.kt | 2 +- .../quartz/nip52Calendar/CalendarEvent.kt | 2 +- .../quartz/nip52Calendar/CalendarRSVPEvent.kt | 2 +- .../nip52Calendar/CalendarTimeSlotEvent.kt | 2 +- .../chat/LiveActivitiesChatMessageEvent.kt | 2 +- .../streaming/LiveActivitiesEvent.kt | 2 +- .../streaming/tags/ParticipantTag.kt | 2 +- .../quartz/nip54Wiki/WikiNoteEvent.kt | 2 +- .../ExternalSignerLauncher.kt | 2 +- .../nip55AndroidSigner/NostrSignerExternal.kt | 4 +- .../quartz/nip56Reports/ReportEvent.kt | 2 +- .../quartz/nip57Zaps/LnZapEvent.kt | 2 +- .../quartz/nip57Zaps/LnZapPrivateEvent.kt | 2 +- .../quartz/nip57Zaps/LnZapRequestEvent.kt | 4 +- .../nip57Zaps/PrivateZapRequestBuilder.kt | 4 +- .../quartz/nip57Zaps/splits/ZapSplitSetup.kt | 2 +- .../quartz/nip58Badges/BadgeAwardEvent.kt | 2 +- .../nip58Badges/BadgeDefinitionEvent.kt | 2 +- .../quartz/nip58Badges/BadgeProfilesEvent.kt | 2 +- .../quartz/nip59Giftwrap/HostStub.kt | 2 +- .../quartz/nip59Giftwrap/WrappedEvent.kt | 2 +- .../quartz/nip59Giftwrap/rumors/Rumor.kt | 4 +- .../nip59Giftwrap/seals/SealedRumorEvent.kt | 2 +- .../nip59Giftwrap/wraps/GiftWrapEvent.kt | 2 +- .../AdvertisedRelayListEvent.kt | 2 +- .../RelayListRecommendationProcessor.kt | 2 +- .../quartz/nip68Picture/IMetaTagBuilderExt.kt | 2 +- .../quartz/nip68Picture/PictureEvent.kt | 2 +- .../nip68Picture/tags/UserAnnotationTag.kt | 2 +- .../quartz/nip71Video/IMetaTagBuilderExt.kt | 2 +- .../quartz/nip71Video/VideoEvent.kt | 2 +- .../quartz/nip71Video/VideoHorizontalEvent.kt | 2 +- .../quartz/nip71Video/VideoVerticalEvent.kt | 2 +- .../quartz/nip71Video/tags/TextTrackTag.kt | 2 +- .../nip72ModCommunities/CommunityListEvent.kt | 2 +- .../approval/CommunityPostApprovalEvent.kt | 2 +- .../definition/CommunityDefinitionEvent.kt | 2 +- .../definition/tags/ModeratorTag.kt | 2 +- .../quartz/nip75ZapGoals/GoalEvent.kt | 2 +- .../nip78AppData/AppSpecificDataEvent.kt | 2 +- .../quartz/nip84Highlights/HighlightEvent.kt | 2 +- .../definition/AppDefinitionEvent.kt | 2 +- .../recommendation/AppRecommendationEvent.kt | 2 +- .../NIP90ContentDiscoveryRequestEvent.kt | 2 +- .../NIP90ContentDiscoveryResponseEvent.kt | 2 +- .../quartz/nip90Dvms/NIP90StatusEvent.kt | 2 +- .../NIP90UserDiscoveryRequestEvent.kt | 2 +- .../NIP90UserDiscoveryResponseEvent.kt | 2 +- .../nip94FileMetadata/FileHeaderEvent.kt | 2 +- .../nip94FileMetadata/IMetaTagBuilderExt.kt | 2 +- .../nip94FileMetadata/TagArrayBuilderExt.kt | 2 +- .../quartz/nip94FileMetadata/tags/ImageTag.kt | 2 +- .../quartz/nip94FileMetadata/tags/ThumbTag.kt | 2 +- .../config/FileServersEvent.kt | 2 +- .../nip98HttpAuth/HTTPAuthorizationEvent.kt | 2 +- .../nip98HttpAuth/tags/PayloadHashTag.kt | 4 +- .../nip99Classifieds/ClassifiedsEvent.kt | 2 +- .../quartz/utils/sha256/Sha256Hasher.kt | 33 +++ .../quartz/utils/sha256/Sha256Pool.kt | 17 +- .../experimental/Nip01SerializerTest.kt | 2 +- 275 files changed, 803 insertions(+), 654 deletions(-) create mode 100644 benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/PoWBenchmark.kt create mode 100644 quartz/src/androidTest/java/com/vitorpamplona/quartz/nip13Pow/PoWMinerTest.kt create mode 100644 quartz/src/androidTest/java/com/vitorpamplona/quartz/nip13Pow/PoWRankEvaluatorTest.kt rename quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/{ => core}/HexKey.kt (96%) rename quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/{ => crypto}/EventHasher.kt (91%) delete mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/experimental/Nip01Serializer.kt rename quartz/src/{androidTest/java/com/vitorpamplona/quartz/nip13Pow/PoWRankProcessorTest.kt => main/java/com/vitorpamplona/quartz/nip13Pow/miner/ByteArrayExt.kt} (55%) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/miner/MiningBuffer.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/miner/PoWMiner.kt rename quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/{PoWRankProcessor.kt => miner/PoWRankEvaluator.kt} (60%) rename quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/{ => tags}/PoWTag.kt (98%) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/utils/sha256/Sha256Hasher.kt diff --git a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ImageUploadTesting.kt b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ImageUploadTesting.kt index 2b1c8becbc..138a46c337 100644 --- a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ImageUploadTesting.kt +++ b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ImageUploadTesting.kt @@ -34,8 +34,8 @@ import com.vitorpamplona.amethyst.service.uploads.nip96.ServerInfoRetriever 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.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair -import com.vitorpamplona.quartz.nip01Core.toHexKey import com.vitorpamplona.quartz.utils.sha256 import junit.framework.TestCase.assertEquals import junit.framework.TestCase.fail diff --git a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/UrlUserTagTransformationTest.kt b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/UrlUserTagTransformationTest.kt index cdbab488b1..263361fb8d 100644 --- a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/UrlUserTagTransformationTest.kt +++ b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/UrlUserTagTransformationTest.kt @@ -27,8 +27,8 @@ 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.core.toHexKey import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata -import com.vitorpamplona.quartz.nip01Core.toHexKey import com.vitorpamplona.quartz.nip19Bech32.decodePublicKey import org.junit.Assert.assertEquals import org.junit.Test diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index 514a8d696a..793881ef22 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -45,13 +45,13 @@ import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow 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.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray +import com.vitorpamplona.quartz.nip01Core.core.toHexKey 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.settings.ChatMessageRelayListEvent import com.vitorpamplona.quartz.nip19Bech32.toNpub diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt index 25091ccb32..8f028fb72e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt @@ -57,7 +57,7 @@ import com.vitorpamplona.amethyst.service.ots.OkHttpCalendarBuilder 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.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip03Timestamp.OtsResolver import com.vitorpamplona.quartz.nip03Timestamp.ots.OpenTimestamps import com.vitorpamplona.quartz.nip19Bech32.bech32.bechToBytes 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 712ff187f8..bd52a61d59 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -76,10 +76,10 @@ 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.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray 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 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 a3fb08daa8..7ea3514436 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt @@ -30,11 +30,11 @@ import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow 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.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.toHexKey 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.settings.ChatMessageRelayListEvent import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt index 5a2b43d434..7305d40300 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt @@ -26,8 +26,8 @@ import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.ui.note.njumpLink import com.vitorpamplona.ammolite.relays.Relay import com.vitorpamplona.ammolite.relays.RelayStats -import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent import kotlinx.coroutines.flow.MutableStateFlow 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 ded1755e69..a7dd1365f2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt @@ -30,11 +30,10 @@ 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.core.HexKey import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress -import com.vitorpamplona.quartz.nip19Bech32.toNAddr import com.vitorpamplona.quartz.nip19Bech32.toNEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip28PublicChat.base.ChannelData diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Chatroom.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Chatroom.kt index dbf77b216f..77b71ebe51 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Chatroom.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Chatroom.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.model import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent import com.vitorpamplona.quartz.nip14Subject.subject import com.vitorpamplona.quartz.utils.TimeUtils 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 c7aee6dffd..e29ec5337b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -47,11 +47,11 @@ 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.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.HexKey import com.vitorpamplona.quartz.nip01Core.core.tagValueContains import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag 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 5d1f1d9db9..2453a4bbfb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -41,9 +41,9 @@ 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.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address @@ -57,7 +57,6 @@ import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent -import com.vitorpamplona.quartz.nip19Bech32.toNAddr import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ParticipantListBuilder.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ParticipantListBuilder.kt index 2ddcd534dc..39c9683de3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ParticipantListBuilder.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ParticipantListBuilder.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.model -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey class ParticipantListBuilder { private fun addFollowsThatDirectlyParticipateOnToSet( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ThreadLevelCalculator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ThreadLevelCalculator.kt index f8d424e532..4dba1c2730 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ThreadLevelCalculator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ThreadLevelCalculator.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.model -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent import java.lang.Long.min 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 a07f3d3c52..d43d32d706 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt @@ -32,7 +32,7 @@ import com.vitorpamplona.ammolite.relays.BundledUpdate 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.core.HexKey import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata import com.vitorpamplona.quartz.nip01Core.tags.geohash.isTaggedGeoHash diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/Base64Image.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/Base64Image.kt index 5727852847..d6a5520a57 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/Base64Image.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/Base64Image.kt @@ -34,7 +34,7 @@ import coil3.key.Keyer import coil3.request.ImageRequest import coil3.request.Options import com.vitorpamplona.amethyst.commons.richtext.RichTextParser.Companion.base64contentPattern -import com.vitorpamplona.quartz.nip01Core.toHexKey +import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.utils.sha256.sha256 import java.util.Base64 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/CashuProcessor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/CashuProcessor.kt index a2d20134a6..165995c30c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/CashuProcessor.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/CashuProcessor.kt @@ -31,7 +31,7 @@ import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager import com.vitorpamplona.amethyst.ui.components.GenericLoadable import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.quartz.nip01Core.toHexKey +import com.vitorpamplona.quartz.nip01Core.core.toHexKey import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.serialization.ExperimentalSerializationApi 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 7032f87614..ff03bb5596 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt @@ -35,8 +35,8 @@ import com.vitorpamplona.quartz.experimental.edits.PrivateOutboxRelayListEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent 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.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent 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 f41c8c7408..0b8a3ee7b2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrSearchEventOrUserDataSource.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrSearchEventOrUserDataSource.kt @@ -30,11 +30,11 @@ import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStory 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.core.hexToByteArray +import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.crypto.Nip01 -import com.vitorpamplona.quartz.nip01Core.hexToByteArray 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 import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress 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 c8151e4684..2f8db2a447 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 @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.service.uploads import android.content.Context import android.net.Uri import androidx.core.net.toUri -import com.vitorpamplona.quartz.nip01Core.toHexKey +import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip17Dm.files.encryption.NostrCipher import com.vitorpamplona.quartz.utils.sha256.sha256 import java.io.File 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 173a46eb49..2203ae4599 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 @@ -30,7 +30,7 @@ import android.os.Build import android.util.Log import com.vitorpamplona.amethyst.commons.blurhash.toBlurhash import com.vitorpamplona.amethyst.service.Blurhash -import com.vitorpamplona.quartz.nip01Core.toHexKey +import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag import com.vitorpamplona.quartz.utils.sha256.sha256 import kotlinx.coroutines.CancellationException 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 39a35280e3..347d374275 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 @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.uploads -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag data class MediaUploadResult( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt index ac5a33fad3..8e76e1a1bb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt @@ -36,8 +36,8 @@ import com.vitorpamplona.amethyst.service.uploads.MediaUploadResult import com.vitorpamplona.amethyst.service.uploads.nip96.randomChars import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.blossom.BlossomAuthorizationEvent -import com.vitorpamplona.quartz.nip01Core.HexKey -import com.vitorpamplona.quartz.nip01Core.toHexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.utils.sha256.sha256 import okhttp3.MediaType.Companion.toMediaType import okhttp3.Request 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 beb7bab1a8..4b7eb7bdcf 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 @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.ui.actions 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.core.HexKey import com.vitorpamplona.quartz.nip01Core.crypto.Nip01 import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser import com.vitorpamplona.quartz.nip19Bech32.bech32.Bech32 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 e4bc3bfa33..2a69173b36 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 @@ -67,9 +67,9 @@ 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.core.HexKey import com.vitorpamplona.quartz.nip01Core.tags.events.ETag import com.vitorpamplona.quartz.nip01Core.tags.events.eTags import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohash diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/UrlUserTagTransformation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/UrlUserTagTransformation.kt index fc7d0e468e..9b6bb948e6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/UrlUserTagTransformation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/UrlUserTagTransformation.kt @@ -31,7 +31,7 @@ import androidx.compose.ui.text.input.TransformedText import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.style.TextDecoration import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.quartz.nip01Core.toHexKey +import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip19Bech32.decodePublicKey import kotlin.coroutines.cancellation.CancellationException 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 f7dacd37a9..0e82326b76 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 @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.ui.actions.relays 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.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.RelayStat @Immutable 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 dc3757935a..7ebb6981d9 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 @@ -63,8 +63,8 @@ import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.note.LoadChannel import com.vitorpamplona.amethyst.ui.note.njumpLink import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList import com.vitorpamplona.quartz.nip02FollowList.ImmutableListOfLists import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent 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 eec79997b4..a7cf24bac6 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 @@ -93,7 +93,7 @@ import com.vitorpamplona.amethyst.ui.theme.Size30dp import com.vitorpamplona.amethyst.ui.theme.Size75dp import com.vitorpamplona.amethyst.ui.theme.hashVerifierMark import com.vitorpamplona.amethyst.ui.theme.imageModifier -import com.vitorpamplona.quartz.nip01Core.toHexKey +import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag 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 6c8e6561de..f8108d057b 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 @@ -24,7 +24,7 @@ import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.tags.addressables.isTaggedAddressableNote import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent 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 3333f97164..7809871d57 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 @@ -25,7 +25,7 @@ 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.core.HexKey import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt index c7f924a690..0cfb0aefde 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt @@ -112,7 +112,7 @@ import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.profileContentHeaderModifier import com.vitorpamplona.amethyst.ui.tor.ConnectTorDialog import com.vitorpamplona.ammolite.relays.RelayPoolStatus -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip02FollowList.ImmutableListOfLists 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 f922b7cadb..d8c678e96d 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 @@ -24,9 +24,9 @@ import com.vitorpamplona.amethyst.model.Channel 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.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent 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 ae66ef69fa..66feb26ad0 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 @@ -101,7 +101,7 @@ 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.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.ParticipantTag diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PubKeyFormatter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PubKeyFormatter.kt index f873e70411..29be5a6779 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PubKeyFormatter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PubKeyFormatter.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.note -import com.vitorpamplona.quartz.nip01Core.HexKey -import com.vitorpamplona.quartz.nip01Core.toHexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.toHexKey fun ByteArray.toShortenHex(): String = toHexKey().toShortenHex() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt index f7b4ff8ee7..bc1d432804 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt @@ -98,7 +98,7 @@ import com.vitorpamplona.amethyst.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.Font14SP import com.vitorpamplona.amethyst.ui.theme.placeholderText -import com.vitorpamplona.quartz.nip01Core.toHexKey +import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip19Bech32.decodePrivateKeyAsHexOrNull import com.vitorpamplona.quartz.nip19Bech32.decodePublicKey import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect 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 4ea7dafb79..8132ccda08 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 @@ -51,7 +51,7 @@ import com.vitorpamplona.amethyst.ui.theme.StdPadding 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.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent @Preview diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/AddInboxRelayForSearchCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/AddInboxRelayForSearchCard.kt index 69b85288a9..83cfc46ff6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/AddInboxRelayForSearchCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/AddInboxRelayForSearchCard.kt @@ -51,7 +51,7 @@ import com.vitorpamplona.amethyst.ui.theme.StdPadding 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.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent @Preview 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 e7bc610267..de6814f1a7 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 @@ -45,7 +45,7 @@ import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel 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.core.HexKey import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList @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 b8f85b4f53..4e796e5df3 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 @@ -36,11 +36,11 @@ import com.vitorpamplona.amethyst.service.Nip05NostrAddressVerifier import com.vitorpamplona.amethyst.ui.tor.TorSettings import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow import com.vitorpamplona.ammolite.relays.Constants +import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray +import com.vitorpamplona.quartz.nip01Core.core.toHexKey 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.ContactListEvent import com.vitorpamplona.quartz.nip02FollowList.ContactTag import com.vitorpamplona.quartz.nip02FollowList.ReadWrite diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountBackupDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountBackupDialog.kt index 5bfff3424d..185e759003 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountBackupDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountBackupDialog.kt @@ -102,7 +102,7 @@ import com.vitorpamplona.amethyst.ui.theme.ButtonPadding import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow import com.vitorpamplona.amethyst.ui.theme.grayText import com.vitorpamplona.amethyst.ui.theme.placeholderText -import com.vitorpamplona.quartz.nip01Core.toHexKey +import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip19Bech32.toNsec import com.vitorpamplona.quartz.nip49PrivKeyEnc.Nip49 import kotlinx.coroutines.CoroutineScope 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 f14583fe40..9f9f6fbcf6 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 @@ -74,9 +74,9 @@ import com.vitorpamplona.amethyst.ui.tor.TorSettings import com.vitorpamplona.ammolite.relays.BundledInsert 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.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address 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 f4c7d4bfba..796a5aa024 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 @@ -43,8 +43,8 @@ import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.navigation.Nav 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.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent 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 1e6edaaa77..391853bc16 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 @@ -79,7 +79,7 @@ import com.vitorpamplona.amethyst.ui.theme.AccountPictureModifier 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.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent 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 aef6b48e77..22d33584a5 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 @@ -34,7 +34,7 @@ import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.ui.note.showAmountInteger import com.vitorpamplona.amethyst.ui.note.showCount import com.vitorpamplona.ammolite.relays.BundledInsert -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/service/zaps/UserProfileZapsFeedFilterTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/service/zaps/UserProfileZapsFeedFilterTest.kt index ba03be0d28..18c6e19ead 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/service/zaps/UserProfileZapsFeedFilterTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/service/zaps/UserProfileZapsFeedFilterTest.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.service.zaps import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.dal.UserProfileZapsFeedFilter -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import io.mockk.every import io.mockk.mockk 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 19d667c7c1..65f4018ee9 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 @@ -25,7 +25,7 @@ import com.vitorpamplona.amethyst.model.LocalCache 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.core.HexKey import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import io.mockk.MockKAnnotations 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 1f6c577878..4a34dda591 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 @@ -21,8 +21,8 @@ package com.vitorpamplona.ammolite.relays.filters 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.core.HexKey import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.filters.FilterMatcher diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/CacheBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/CacheBenchmark.kt index 447e7824d1..a20ce449e1 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/CacheBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/CacheBenchmark.kt @@ -26,8 +26,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.amethyst.commons.data.LargeCache -import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper import org.junit.Assert.assertTrue import org.junit.Rule diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/EventCmdSerializerBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/EventCmdSerializerBenchmark.kt index c368eb3908..db54fc9c33 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/EventCmdSerializerBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/EventCmdSerializerBenchmark.kt @@ -25,11 +25,10 @@ import androidx.benchmark.junit4.measureRepeated import androidx.test.ext.junit.runners.AndroidJUnit4 import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.experimental.Nip01Serializer +import com.vitorpamplona.quartz.utils.sha256.Sha256Hasher import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import java.security.MessageDigest /** * Benchmark, which will execute on an Android device. @@ -50,16 +49,6 @@ class EventCmdSerializerBenchmark { } } - @Test - fun eventSerializerManualTest() { - val event = Event.fromJson(largeKind1Event) - - benchmarkRule.measureRepeated { - val mapper = Nip01Serializer.StringWriter() - Nip01Serializer().serializeEventInto(event, mapper) - } - } - val specialEncoders = "Test\b\bTest\n\nTest\t\tTest\u000c\u000cTest\r\rTest\\Test\\\\Test\"Test/Test//Test" @@ -71,28 +60,12 @@ class EventCmdSerializerBenchmark { } } - @Test - fun jsonStringEncoderOurs() { - val serializer = Nip01Serializer() - benchmarkRule.measureRepeated { - serializer.escapeStringInto(specialEncoders, Nip01Serializer.StringWriter()) - } - } - @Test fun jsonStringEncoderSha256Jackson() { val jsonMapper = jacksonObjectMapper() benchmarkRule.measureRepeated { - val digest = MessageDigest.getInstance("SHA-256") - digest.update(jsonMapper.writeValueAsString(specialEncoders).toByteArray()) - } - } - - @Test - fun jsonStringEncoderSha256Ours() { - val serializer = Nip01Serializer() - benchmarkRule.measureRepeated { - serializer.escapeStringInto(specialEncoders, Nip01Serializer.BufferedDigestWriter(MessageDigest.getInstance("SHA-256"))) + val digest = Sha256Hasher() + digest.hash(jsonMapper.writeValueAsString(specialEncoders).toByteArray()) } } } 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 00900de95f..3bf8960596 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapReceivingBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapReceivingBenchmark.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.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray 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.nip01Core.tags.people.PTag diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/LargeCacheBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/LargeCacheBenchmark.kt index 90ce0661b4..d26cf3613d 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/LargeCacheBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/LargeCacheBenchmark.kt @@ -26,8 +26,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.amethyst.commons.data.LargeCache -import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper import org.junit.Assert.assertTrue import org.junit.Rule diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/PoWBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/PoWBenchmark.kt new file mode 100644 index 0000000000..94011874b9 --- /dev/null +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/PoWBenchmark.kt @@ -0,0 +1,97 @@ +/** + * 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.benchmark + +import androidx.benchmark.junit4.BenchmarkRule +import androidx.benchmark.junit4.measureRepeated +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray +import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair +import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate +import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync +import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent +import com.vitorpamplona.quartz.nip13Pow.miner.PoWMiner +import com.vitorpamplona.quartz.nip13Pow.miner.PoWRankEvaluator +import com.vitorpamplona.quartz.nip13Pow.pow +import com.vitorpamplona.quartz.nip13Pow.powNonce +import org.junit.Assert.assertEquals +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Benchmark, which will execute on an Android device. + * + * The body of [BenchmarkRule.measureRepeated] is measured in a loop, and Studio will output the + * result. Modify your code to see how it affects performance. + */ +@RunWith(AndroidJUnit4::class) +class PoWBenchmark { + @get:Rule + val benchmarkRule = BenchmarkRule() + + val baseTemplate = + EventTemplate( + 1683596206, + TextNoteEvent.KIND, + 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 ", + ) + + @Test + fun generatePow() { + val signer = NostrSignerSync(KeyPair()) + var template: EventTemplate? = null + benchmarkRule.measureRepeated { + template = PoWMiner.run(baseTemplate, signer.pubKey, 5) + } + val event = signer.sign(template!!) + assertEquals(5, event?.pow()) + println("POW: " + event?.powNonce()?.nonce) + } + + @Test + fun setPoWCalculationHex() { + benchmarkRule.measureRepeated { + assertEquals(26, PoWRankEvaluator.calculatePowRankOf("00000026c91e9fc75fdb95b367776e2594b931cebda6d5ca3622501006669c9e")) + } + } + + @Test + fun setPoWCalculationBytes() { + val bytes = "00000026c91e9fc75fdb95b367776e2594b931cebda6d5ca3622501006669c9e".hexToByteArray() + benchmarkRule.measureRepeated { + assertEquals(26, PoWRankEvaluator.calculatePowRankOf(bytes)) + } + } +} diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/Sha256Benchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/Sha256Benchmark.kt index 7ffff23ae9..5c176a8aa2 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/Sha256Benchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/Sha256Benchmark.kt @@ -23,8 +23,8 @@ 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.EventHasher import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.crypto.EventHasher import com.vitorpamplona.quartz.utils.sha256.sha256 import junit.framework.TestCase.assertNotNull import org.junit.Rule diff --git a/commons/src/main/java/com/vitorpamplona/amethyst/commons/data/DeletionIndex.kt b/commons/src/main/java/com/vitorpamplona/amethyst/commons/data/DeletionIndex.kt index 984de380ed..2fa763348d 100644 --- a/commons/src/main/java/com/vitorpamplona/amethyst/commons/data/DeletionIndex.kt +++ b/commons/src/main/java/com/vitorpamplona/amethyst/commons/data/DeletionIndex.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.commons.data -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.HexKey import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent class DeletionIndex { diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/bloom/BloomFilter.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/bloom/BloomFilter.kt index 1a9b1bacdf..7da61abaf8 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/bloom/BloomFilter.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/bloom/BloomFilter.kt @@ -21,17 +21,17 @@ package com.vitorpamplona.quartz.bloom import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray import com.vitorpamplona.quartz.nip01Core.crypto.Nip01 -import com.vitorpamplona.quartz.nip01Core.hexToByteArray import com.vitorpamplona.quartz.utils.RandomInstance +import com.vitorpamplona.quartz.utils.sha256.Sha256Hasher import junit.framework.TestCase.assertEquals import junit.framework.TestCase.assertFalse import junit.framework.TestCase.assertTrue import org.junit.Test import org.junit.runner.RunWith import java.math.BigInteger -import java.security.MessageDigest import java.util.Base64 import java.util.BitSet import java.util.concurrent.locks.ReentrantReadWriteLock @@ -44,7 +44,7 @@ class BloomFilter( private val bits: BitSet = BitSet(size), private val salt: ByteArray = RandomInstance.bytes(8), ) { - private val hash = MessageDigest.getInstance("SHA-256") + private val hash = Sha256Hasher() private val lock = ReentrantReadWriteLock() fun add(value: HexKey) = add(value.hexToByteArray()) @@ -83,7 +83,7 @@ class BloomFilter( fun hash( seed: Int, value: ByteArray, - ) = BigInteger(1, hash.digest(value + salt + seed.toByte())) + ) = BigInteger(1, hash.hash(value + salt + seed.toByte())) .remainder(BigInteger.valueOf(size.toLong())) .toInt() 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 7566c29ea2..16acec1024 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip01Core/Nip01Test.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip01Core/Nip01Test.kt @@ -21,9 +21,11 @@ package com.vitorpamplona.quartz.nip01Core import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray +import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.crypto.Nip01 import com.vitorpamplona.quartz.utils.Secp256k1Instance -import com.vitorpamplona.quartz.utils.sha256 +import com.vitorpamplona.quartz.utils.sha256.sha256 import org.junit.Assert.assertEquals import org.junit.Assert.assertNotEquals import org.junit.Assert.assertTrue diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip01Core/crypto/Nip01CryptoTest.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip01Core/crypto/Nip01CryptoTest.kt index 9835f3b0f5..8789248a53 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip01Core/crypto/Nip01CryptoTest.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip01Core/crypto/Nip01CryptoTest.kt @@ -21,8 +21,8 @@ package com.vitorpamplona.quartz.nip01Core.crypto import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.vitorpamplona.quartz.nip01Core.hexToByteArray -import com.vitorpamplona.quartz.nip01Core.toHexKey +import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray +import com.vitorpamplona.quartz.nip01Core.core.toHexKey import org.junit.Assert.assertEquals import org.junit.Test import org.junit.runner.RunWith diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip04Dm/EncryptionTest.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip04Dm/EncryptionTest.kt index e25755e291..b79267bf31 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip04Dm/EncryptionTest.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip04Dm/EncryptionTest.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.quartz.nip04Dm +import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray +import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.crypto.Nip01 -import com.vitorpamplona.quartz.nip01Core.hexToByteArray -import com.vitorpamplona.quartz.nip01Core.toHexKey import com.vitorpamplona.quartz.nip04Dm.crypto.EncryptedInfo import com.vitorpamplona.quartz.nip04Dm.crypto.Encryption import junit.framework.TestCase.assertEquals diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip06KeyDerivation/Bip32SeedDerivationTest.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip06KeyDerivation/Bip32SeedDerivationTest.kt index 722372b3c1..9d093fd3b1 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip06KeyDerivation/Bip32SeedDerivationTest.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip06KeyDerivation/Bip32SeedDerivationTest.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.quartz.nip06KeyDerivation import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.vitorpamplona.quartz.nip01Core.toHexKey +import com.vitorpamplona.quartz.nip01Core.core.toHexKey import junit.framework.TestCase.assertEquals import org.junit.Test import org.junit.runner.RunWith diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip06KeyDerivation/Bip39MnemonicsTest.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip06KeyDerivation/Bip39MnemonicsTest.kt index 2e1d957296..5ffb1a116c 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip06KeyDerivation/Bip39MnemonicsTest.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip06KeyDerivation/Bip39MnemonicsTest.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.quartz.nip06KeyDerivation import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -import com.vitorpamplona.quartz.nip01Core.toHexKey +import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.utils.Hex import com.vitorpamplona.quartz.utils.RandomInstance import junit.framework.TestCase.assertEquals diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip06KeyDerivation/Nip06Test.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip06KeyDerivation/Nip06Test.kt index 040cfa7581..b8eee92b22 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip06KeyDerivation/Nip06Test.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip06KeyDerivation/Nip06Test.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.quartz.nip06KeyDerivation import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.vitorpamplona.quartz.nip01Core.toHexKey +import com.vitorpamplona.quartz.nip01Core.core.toHexKey import junit.framework.TestCase.assertEquals import org.junit.Ignore import org.junit.Test diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip13Pow/PoWMinerTest.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip13Pow/PoWMinerTest.kt new file mode 100644 index 0000000000..80de76babe --- /dev/null +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip13Pow/PoWMinerTest.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.nip13Pow + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair +import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate +import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync +import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent +import com.vitorpamplona.quartz.nip13Pow.miner.PoWMiner +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class PoWMinerTest { + val baseTemplate = + EventTemplate( + 1683596206, + TextNoteEvent.KIND, + 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 ", + ) + + @Test + fun mine() { + val signer = NostrSignerSync(KeyPair()) + println("Starting") + val template = PoWMiner.run(baseTemplate, signer.pubKey, 25) + println("Finished") + + val event = signer.sign(template) + + println(event?.toJson()) + + assertEquals(25, event!!.pow()) + } +} diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip13Pow/PoWRankEvaluatorTest.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip13Pow/PoWRankEvaluatorTest.kt new file mode 100644 index 0000000000..2ced5d1296 --- /dev/null +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip13Pow/PoWRankEvaluatorTest.kt @@ -0,0 +1,73 @@ +/** + * 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.nip13Pow + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray +import com.vitorpamplona.quartz.nip13Pow.miner.PoWRankEvaluator +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class PoWRankEvaluatorTest { + val tests = + mapOf( + "000006d8c378af1779d2feebc7603a125d99eca0ccf1085959b307f64e5dd358" to 21, + "6bf5b4f434813c64b523d2b0e6efe18f3bd0cbbd0a5effd8ece9e00fd2531996" to 1, + "00003479309ecdb46b1c04ce129d2709378518588bed6776e60474ebde3159ae" to 18, + "01a76167d41add96be4959d9e618b7a35f26551d62c43c11e5e64094c6b53c83" to 7, + "ac4f44bae06a45ebe88cfbd3c66358750159650a26c0d79e8ccaa92457fca4f6" to 0, + "0000000000000000006cfbd3c66358750159650a26c0d79e8ccaa92457fca4f6" to 73, + "00000026c91e9fc75fdb95b367776e2594b931cebda6d5ca3622501006669c9e" to 26, + ) + + @Test + fun testHex() { + tests.forEach { + assertEquals(it.value, PoWRankEvaluator.calculatePowRankOf(it.key)) + } + } + + @Test + fun testByte() { + tests.forEach { + assertEquals(it.value, PoWRankEvaluator.calculatePowRankOf(it.key.hexToByteArray())) + } + } + + val commitmentTest = "00000026c91e9fc75fdb95b367776e2594b931cebda6d5ca3622501006669c9e" + + @Test + fun setPoWIfCommited25() { + assertEquals(25, PoWRankEvaluator.compute(commitmentTest, 25)) + } + + @Test + fun setPoWIfCommited26() { + assertEquals(26, PoWRankEvaluator.compute(commitmentTest, 26)) + } + + @Test + fun setPoWIfCommited27() { + assertEquals(26, PoWRankEvaluator.compute(commitmentTest, 27)) + } +} 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 c755059871..5214c0e054 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip17Dm/AESGCMTest.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip17Dm/AESGCMTest.kt @@ -22,7 +22,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.nip01Core.core.hexToByteArray import com.vitorpamplona.quartz.nip17Dm.files.encryption.AESGCM import junit.framework.TestCase.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 90b4f8b834..7df8c7a050 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip19Bech32/NIP19EmbedTests.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip19Bech32/NIP19EmbedTests.kt @@ -23,8 +23,8 @@ package com.vitorpamplona.quartz.nip19Bech32 import androidx.test.ext.junit.runners.AndroidJUnit4 import com.vitorpamplona.quartz.experimental.medical.FhirResourceEvent import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray 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 diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip44Encryption/Nip44v1Test.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip44Encryption/Nip44v1Test.kt index a3bc8b6453..611c9abf5f 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip44Encryption/Nip44v1Test.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip44Encryption/Nip44v1Test.kt @@ -21,10 +21,10 @@ package com.vitorpamplona.quartz.nip44Encryption import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray +import com.vitorpamplona.quartz.nip01Core.core.toHexKey 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 org.junit.Assert.assertEquals import org.junit.Test import org.junit.runner.RunWith 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 104371cddf..dba7e58cfb 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip44Encryption/Nip44v2Test.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip44Encryption/Nip44v2Test.kt @@ -23,12 +23,12 @@ 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.core.hexToByteArray +import com.vitorpamplona.quartz.nip01Core.core.toHexKey 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.RandomInstance -import com.vitorpamplona.quartz.utils.sha256 +import com.vitorpamplona.quartz.utils.sha256.sha256 import junit.framework.TestCase.assertEquals import junit.framework.TestCase.assertNotNull import junit.framework.TestCase.assertNull diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip49PrivKeyEnc/NIP49Test.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip49PrivKeyEnc/NIP49Test.kt index f7876a08ee..8c39bf8654 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip49PrivKeyEnc/NIP49Test.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip49PrivKeyEnc/NIP49Test.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.quartz.nip49PrivKeyEnc import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.vitorpamplona.quartz.nip01Core.toHexKey +import com.vitorpamplona.quartz.nip01Core.core.toHexKey import junit.framework.TestCase.assertEquals import junit.framework.TestCase.assertNotNull import junit.framework.TestCase.fail diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip55AndroidSigner/SignStringTest.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip55AndroidSigner/SignStringTest.kt index bef7a3f603..cbf2311167 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip55AndroidSigner/SignStringTest.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip55AndroidSigner/SignStringTest.kt @@ -21,10 +21,10 @@ package com.vitorpamplona.quartz.nip55AndroidSigner import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray +import com.vitorpamplona.quartz.nip01Core.core.toHexKey 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 org.junit.Assert.assertEquals import org.junit.Test import org.junit.runner.RunWith 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 e872503a55..eb696ff7a2 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip57Zaps/PrivateZapTests.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip57Zaps/PrivateZapTests.kt @@ -22,10 +22,10 @@ package com.vitorpamplona.quartz.nip57Zaps import androidx.test.ext.junit.runners.AndroidJUnit4 import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.toHexKey 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 import com.vitorpamplona.quartz.nip57Zaps.PrivateZapEncryption.Companion.createEncryptionPrivateKey import com.vitorpamplona.quartz.nip59GiftWraps.wait1SecondForResult import com.vitorpamplona.quartz.utils.Hex 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 9d4d50e3c6..62bcbf3bcc 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip59GiftWraps/GiftWrapEventTest.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip59GiftWraps/GiftWrapEventTest.kt @@ -21,11 +21,11 @@ package com.vitorpamplona.quartz.nip59GiftWraps import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.checkSignature import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/EventFactory.kt b/quartz/src/main/java/com/vitorpamplona/quartz/EventFactory.kt index 3072c450a5..19a1db44f8 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/EventFactory.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/EventFactory.kt @@ -36,9 +36,9 @@ 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.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.crypto.EventHasher import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/blossom/BlossomAuthorizationEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/blossom/BlossomAuthorizationEvent.kt index 2108ac6f27..cc8cd8f3e3 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/blossom/BlossomAuthorizationEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/blossom/BlossomAuthorizationEvent.kt @@ -21,8 +21,8 @@ package com.vitorpamplona.quartz.blossom import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.utils.TimeUtils 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 87c04a6cae..3bb4f13a02 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/blossom/BlossomServersEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/blossom/BlossomServersEvent.kt @@ -21,8 +21,8 @@ package com.vitorpamplona.quartz.blossom import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.BaseReplaceableEvent +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address 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 index d012db4162..396be396a1 100644 --- 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 @@ -24,8 +24,8 @@ 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.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip31Alts.alt diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/track/AudioTrackEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/track/AudioTrackEvent.kt index d7eccfad52..dfd0414018 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/track/AudioTrackEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/track/AudioTrackEvent.kt @@ -26,8 +26,8 @@ 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.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag 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 index 155f0d9195..1701e908f0 100644 --- 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 @@ -21,7 +21,7 @@ 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.HexKey import com.vitorpamplona.quartz.nip01Core.core.Tag import com.vitorpamplona.quartz.nip01Core.core.isNotName import com.vitorpamplona.quartz.nip01Core.tags.people.PubKeyReferenceTag 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 40ed346ccb..bd418eec3a 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 @@ -23,8 +23,8 @@ package com.vitorpamplona.quartz.experimental.edits 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.BaseReplaceableEvent +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArray import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner 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 79dad5979c..f5f25ceff8 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 @@ -21,8 +21,8 @@ package com.vitorpamplona.quartz.experimental.edits import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.events.firstTaggedEvent import com.vitorpamplona.quartz.nip31Alts.AltTag 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 index af5f4e101f..6db5f7739a 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/forks/BaseThreadedEventExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/forks/BaseThreadedEventExt.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.quartz.experimental.forks -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag 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 96518fcb3a..f1aa08c29c 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 @@ -21,8 +21,8 @@ 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.HexKey import com.vitorpamplona.quartz.nip23LongContent.tags.ImageTag import com.vitorpamplona.quartz.nip23LongContent.tags.SummaryTag import com.vitorpamplona.quartz.nip23LongContent.tags.TitleTag 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 50990aabf9..489567c6ab 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 @@ -21,7 +21,7 @@ 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.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate 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 f409a8c49a..9ee1e3f0b9 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 @@ -23,8 +23,8 @@ 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.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.core.builder import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate 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 f647d793d4..341da104aa 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 @@ -21,7 +21,7 @@ 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.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag 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 index 6a14f8e783..652d88f629 100644 --- 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 @@ -21,7 +21,7 @@ package com.vitorpamplona.quartz.experimental.interactiveStories.tags import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.utils.arrayOfNotNull import com.vitorpamplona.quartz.utils.bytesUsedInMemory 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 c03618f33d..0fbbfc0c4c 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 @@ -22,8 +22,8 @@ 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.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip31Alts.alt diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/nip95/data/FileStorageEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/nip95/data/FileStorageEvent.kt index 01af752490..00d51dc0d3 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/nip95/data/FileStorageEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/nip95/data/FileStorageEvent.kt @@ -21,8 +21,8 @@ package com.vitorpamplona.quartz.experimental.nip95.data import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate 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 index 9b8a5b71a7..0baeff223d 100644 --- 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 @@ -23,8 +23,8 @@ 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.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.core.any import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate 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 index 69911a409a..5e0d11845a 100644 --- 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 @@ -20,7 +20,7 @@ */ package com.vitorpamplona.quartz.experimental.nip95.header -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip94FileMetadata.tags.BlurhashTag import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag 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 1fc8486f6f..ee5886cbde 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 @@ -24,8 +24,8 @@ 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.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip31Alts.alt 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 6eace1de7f..ab99c19779 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 @@ -21,7 +21,7 @@ package com.vitorpamplona.quartz.experimental.profileGallery import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip31Alts.AltTag 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 aa9725a8f9..2d67776046 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 @@ -21,8 +21,8 @@ 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.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.core.any import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate 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 index 8023ba0c5b..3a0d22a4a5 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/profileGallery/TagArrayBuilderExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/profileGallery/TagArrayBuilderExt.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.quartz.experimental.profileGallery -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.tags.events.ETag import com.vitorpamplona.quartz.nip94FileMetadata.tags.BlurhashTag 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 f4e70bf4fd..46fe9fe74b 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 @@ -24,7 +24,7 @@ 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.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.core.tagArray import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner 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 f9ad4057c3..d0d2279308 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 @@ -26,7 +26,7 @@ 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.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent 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 3afaf3bc78..052f2b9ba2 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/EventExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/EventExt.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.quartz.nip01Core import android.util.Log import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.crypto.EventHasher import com.vitorpamplona.quartz.nip01Core.crypto.Nip01 import com.vitorpamplona.quartz.utils.Hex 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 16019c7269..4cac7071f1 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 @@ -21,7 +21,6 @@ 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 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 184fe4eea7..24404b5a2e 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 @@ -21,7 +21,6 @@ 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 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 cb2c863e66..5055ef9df1 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 @@ -22,7 +22,6 @@ package com.vitorpamplona.quartz.nip01Core.core import androidx.compose.runtime.Immutable import com.fasterxml.jackson.annotation.JsonProperty -import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.jackson.EventManualSerializer import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/HexKey.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/HexKey.kt similarity index 96% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/HexKey.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/HexKey.kt index b729491b32..caa4536f60 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/HexKey.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/HexKey.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.core import com.vitorpamplona.quartz.utils.Hex 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 77f49af17d..36ba47ae64 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 @@ -20,8 +20,6 @@ */ package com.vitorpamplona.quartz.nip01Core.core -import com.vitorpamplona.quartz.nip01Core.HexKey - typealias TagArray = Array> fun TagArray.builder(initializer: TagArrayBuilder.() -> Unit = {}) = TagArrayBuilder().addAll(this).apply(initializer).build() diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/EventHasher.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/crypto/EventHasher.kt similarity index 91% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/EventHasher.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/crypto/EventHasher.kt index e596772e7b..591b2695b4 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/EventHasher.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/crypto/EventHasher.kt @@ -18,10 +18,12 @@ * AN ACTION OF CONTRACT, TORT OR 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.fasterxml.jackson.databind.node.ArrayNode import com.fasterxml.jackson.databind.node.JsonNodeFactory +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper import com.vitorpamplona.quartz.utils.sha256.sha256 @@ -69,6 +71,8 @@ class EventHasher { content: String, ): ByteArray = sha256(makeJsonForId(pubKey, createdAt, kind, tags, content).toByteArray()) + fun hashId(serializedJsonAsBytes: ByteArray): String = sha256(serializedJsonAsBytes).toHexKey() + fun hashId( pubKey: HexKey, createdAt: Long, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/crypto/KeyPair.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/crypto/KeyPair.kt index 4dc020aaa5..048309e560 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/crypto/KeyPair.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/crypto/KeyPair.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.quartz.nip01Core.crypto -import com.vitorpamplona.quartz.nip01Core.toHexKey +import com.vitorpamplona.quartz.nip01Core.core.toHexKey class KeyPair( privKey: ByteArray? = null, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/experimental/Nip01Serializer.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/experimental/Nip01Serializer.kt deleted file mode 100644 index e4b89fbc0c..0000000000 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/experimental/Nip01Serializer.kt +++ /dev/null @@ -1,277 +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.nip01Core.experimental - -import com.vitorpamplona.quartz.nip01Core.core.Event -import java.nio.ByteBuffer -import java.nio.CharBuffer -import java.nio.charset.CodingErrorAction -import java.security.MessageDigest - -class Nip01Serializer { - interface Writer { - fun append( - value: ByteArray, - offset: Int, - length: Int, - ) - - fun append( - value: String, - offset: Int, - length: Int, - ) - - fun append(value: ByteArray) - - fun append(value: Byte) - - fun dump() - } - - class BufferedDigestWriter( - val digest: MessageDigest, - ) : Writer { - val utf8Encoder = - Charsets.UTF_8 - .newEncoder() - .onMalformedInput(CodingErrorAction.IGNORE) - .onUnmappableCharacter(CodingErrorAction.IGNORE) - - companion object { - const val BUFFER_SIZE = 128 - } - - private val innerBuffer = ByteArray(BUFFER_SIZE) - private val byteBuffer = ByteBuffer.wrap(innerBuffer) - - override fun append(value: Byte) { - if (byteBuffer.position() == byteBuffer.capacity()) { - dump() - } - - byteBuffer.put(value) - } - - override fun append(value: ByteArray) { - if (value.size >= BUFFER_SIZE) { - dump() - // don't cache if the cache is smaller than the value - digest.update(value) - } else { - if (value.size > byteBuffer.remaining()) { - dump() - } - - value.copyInto(innerBuffer, byteBuffer.position(), 0, value.size) - byteBuffer.position(byteBuffer.position() + value.size) - } - } - - override fun append( - value: ByteArray, - offset: Int, - length: Int, - ) { - if (value.size >= BUFFER_SIZE) { - dump() - // don't cache if the cache is smaller than the value - digest.update(value, offset, length) - } else { - if (length > byteBuffer.remaining()) { - dump() - } - - value.copyInto(innerBuffer, byteBuffer.position(), offset, offset + length) - byteBuffer.position(byteBuffer.position() + value.size) - } - } - - override fun append( - value: String, - offset: Int, - length: Int, - ) { - val toEncode = CharBuffer.wrap(value, offset, offset + length) - while (toEncode.hasRemaining()) { - val result = utf8Encoder.encode(toEncode, byteBuffer, false) - if (result.isOverflow) { - dump() - } - } - } - - override fun dump() { - if (byteBuffer.position() > 0) { - digest.update(innerBuffer, 0, byteBuffer.position()) - byteBuffer.clear() - } - } - } - - class StringWriter : Writer { - private val stringBuilder: StringBuilder = StringBuilder() - - override fun append( - value: ByteArray, - offset: Int, - length: Int, - ) { - stringBuilder.append(value.decodeToString(offset, offset + length)) - } - - override fun append(value: ByteArray) { - stringBuilder.append(value.decodeToString()) - } - - override fun append( - value: String, - offset: Int, - length: Int, - ) { - stringBuilder.append(value, offset, offset + length) - } - - override fun append(value: Byte) { - stringBuilder.append(value.toInt().toChar()) - } - - override fun toString(): String = stringBuilder.toString() - - override fun dump() { - } - } - - companion object { - private const val DOUBLE_QUOTE_ASCII = 0x22 - private const val BACKLASH_ASCII = 0x5C - private const val TAB_ASCII = 0x09 - private const val BACKSPACE_ASCII = 0x08 - private const val NEWLINE_ASCII = 0x0A - private const val RETURN_ASCII = 0x0D - private const val FORM_FEED_ASCII = 0x0C - - private val ESCAPED_DOUBLE_QUOTE = "\\\"".toByteArray() - private val ESCAPED_DOUBLE_BACKLASH = "\\\\".toByteArray() - private val ESCAPED_TAB = "\\t".toByteArray() - private val ESCAPED_BACKSPACE = "\\b".toByteArray() - private val ESCAPED_NEW_LINE = "\\n".toByteArray() - private val ESCAPED_RETURN = "\\r".toByteArray() - private val ESCAPED_FORM_FEED = "\\f".toByteArray() - - val ARRAY_ZERO_COMMA_QUOTE = "[0,\"".toByteArray() - val COMMA = ",".toByteArray() - val QUOTE = "\"".toByteArray() - val QUOTE_COMMA = "\",".toByteArray() - val QUOTE_COMMA_QUOTE = "\",\"".toByteArray() - val COMMA_OPEN_ARRAY = ",[".toByteArray() - val COMMA_OPEN_ARRAY_QUOTE = ",[\"".toByteArray() - val OPEN_ARRAY = "[".toByteArray() - val OPEN_ARRAY_QUOTE = "[\"".toByteArray() - val QUOTE_CLOSE_ARRAY = "\"]".toByteArray() - val CLOSE_ARRAY_COMMA_QUOTE = "],\"".toByteArray() - - private val MAPPER = Array(255) { null } - - init { - for (i in 0 until 0x1F) { - MAPPER[i] = String.format("\\u%04x", i.toByte()).toByteArray() - } - - MAPPER[DOUBLE_QUOTE_ASCII] = ESCAPED_DOUBLE_QUOTE - MAPPER[BACKLASH_ASCII] = ESCAPED_DOUBLE_BACKLASH - MAPPER[TAB_ASCII] = ESCAPED_TAB - MAPPER[BACKSPACE_ASCII] = ESCAPED_BACKSPACE - MAPPER[NEWLINE_ASCII] = ESCAPED_NEW_LINE - MAPPER[RETURN_ASCII] = ESCAPED_RETURN - MAPPER[FORM_FEED_ASCII] = ESCAPED_FORM_FEED - } - } - - fun escapeStringInto( - value: String, - writer: Writer, - ) { - var lastNormalSequenceStarts = 0 - var lastNormalSequenceLength = 0 - - for (i in value.indices) { - if (value[i].code >= 255) { - lastNormalSequenceLength++ - } else { - val escaped = MAPPER[value[i].code] - if (escaped != null) { - if (lastNormalSequenceLength > 0) { - writer.append(value, lastNormalSequenceStarts, lastNormalSequenceLength) - } - lastNormalSequenceStarts = i + 1 - lastNormalSequenceLength = 0 - writer.append(escaped) - } else { - lastNormalSequenceLength++ - } - } - } - - if (lastNormalSequenceLength > 0) { - if (lastNormalSequenceLength == value.length) { - writer.append(value, 0, value.length) - } else { - writer.append(value, lastNormalSequenceStarts, lastNormalSequenceLength) - } - } - } - - fun serializeEventInto( - event: Event, - writer: Writer, - ) { - writer.append(ARRAY_ZERO_COMMA_QUOTE) - writer.append(event.pubKey.toByteArray()) - writer.append(QUOTE_COMMA) - writer.append(event.createdAt.toString().toByteArray()) - writer.append(COMMA) - writer.append(event.kind.toString().toByteArray()) - writer.append(COMMA_OPEN_ARRAY) - - for (index in event.tags.indices) { - val tag = event.tags[index] - if (index > 0) { - writer.append(COMMA_OPEN_ARRAY_QUOTE) - } else { - writer.append(OPEN_ARRAY_QUOTE) - } - for (sIndex in tag.indices) { - if (sIndex > 0) { - writer.append(QUOTE_COMMA_QUOTE) - } - escapeStringInto(tag[sIndex], writer) - } - writer.append(QUOTE_CLOSE_ARRAY) - } - writer.append(CLOSE_ARRAY_COMMA_QUOTE) - - escapeStringInto(event.content, writer) - writer.append(QUOTE_CLOSE_ARRAY) - - writer.dump() - } -} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/EventManualSerializer.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/EventManualSerializer.kt index 528a3c5f1b..15724e357d 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/EventManualSerializer.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/EventManualSerializer.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.quartz.nip01Core.jackson import com.fasterxml.jackson.databind.node.JsonNodeFactory -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey class EventManualSerializer { companion object { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/metadata/MetadataEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/metadata/MetadataEvent.kt index dec6165214..fce6362dc8 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/metadata/MetadataEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/metadata/MetadataEvent.kt @@ -23,8 +23,8 @@ 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.core.HexKey import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/SimpleClientRelay.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/SimpleClientRelay.kt index ecbe9a21f8..6877384e8d 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/SimpleClientRelay.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/SimpleClientRelay.kt @@ -21,8 +21,8 @@ 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.core.HexKey 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toClient/OkMessage.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toClient/OkMessage.kt index 0d494bf1d4..492114465d 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toClient/OkMessage.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toClient/OkMessage.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.quartz.nip01Core.relay.commands.toClient import com.fasterxml.jackson.databind.JsonNode -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey class OkMessage( val eventId: HexKey, 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 ad0f8998b4..af3f876f39 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 @@ -21,9 +21,9 @@ package com.vitorpamplona.quartz.nip01Core.signers 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.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.crypto.EventHasher import com.vitorpamplona.quartz.nip04Dm.crypto.EncryptedInfo import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent 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 70da4e2f41..c49e52bc29 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 @@ -20,10 +20,10 @@ */ package com.vitorpamplona.quartz.nip01Core.signers -import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.toHexKey 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 b45160e435..ebf0029ce5 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 @@ -22,13 +22,13 @@ package com.vitorpamplona.quartz.nip01Core.signers import android.util.Log 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.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray +import com.vitorpamplona.quartz.nip01Core.core.toHexKey +import com.vitorpamplona.quartz.nip01Core.crypto.EventHasher 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.nip04Dm.crypto.Nip04 import com.vitorpamplona.quartz.nip44Encryption.Nip44 import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent @@ -39,6 +39,8 @@ class NostrSignerSync( val keyPair: KeyPair, val pubKey: HexKey = keyPair.pubKey.toHexKey(), ) { + fun sign(ev: EventTemplate) = signNormal(ev.createdAt, ev.kind, ev.tags, ev.content) + fun sign( createdAt: Long, kind: Int, 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 df4df4d191..36eba88da6 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 @@ -21,7 +21,7 @@ package com.vitorpamplona.quartz.nip01Core.tags.addressables import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.match import com.vitorpamplona.quartz.nip01Core.core.name import com.vitorpamplona.quartz.nip01Core.core.value 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 index cbcbd3b75c..a030e3bca5 100644 --- 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 @@ -21,7 +21,7 @@ package com.vitorpamplona.quartz.nip01Core.tags.addressables import android.util.Log -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.utils.Hex import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.pointerSizeInBytes 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 0fb707e07a..d4d58791c0 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 @@ -21,7 +21,7 @@ package com.vitorpamplona.quartz.nip01Core.tags.events import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent import com.vitorpamplona.quartz.utils.arrayOfNotNull import com.vitorpamplona.quartz.utils.bytesUsedInMemory diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/events/EventExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/events/EventExt.kt index eea195ef91..883b722d4e 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/events/EventExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/events/EventExt.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.quartz.nip01Core.tags.events -import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey fun Event.forEachTaggedEventId(onEach: (eventId: HexKey) -> Unit) = tags.forEachTaggedEventId(onEach) 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 index 4196cd21eb..8728ff3b4b 100644 --- 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 @@ -20,7 +20,7 @@ */ package com.vitorpamplona.quartz.nip01Core.tags.events -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey class EventReference( val eventId: HexKey, 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 index b8af54f12a..b10a836f42 100644 --- 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 @@ -20,7 +20,7 @@ */ package com.vitorpamplona.quartz.nip01Core.tags.events -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey interface GenericETag { val eventId: HexKey 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 f4aa37bfde..88c11e6fad 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 @@ -20,11 +20,10 @@ */ package com.vitorpamplona.quartz.nip01Core.tags.events -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArray import com.vitorpamplona.quartz.nip01Core.core.any import com.vitorpamplona.quartz.nip01Core.core.forEachTagged -import com.vitorpamplona.quartz.nip01Core.core.isTagged import com.vitorpamplona.quartz.nip01Core.core.mapValueTagged fun TagArray.forEachTaggedEventId(onEach: (eventId: HexKey) -> Unit) = this.forEachTagged(ETag.TAG_NAME, onEach) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/hashtags/EventExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/hashtags/EventExt.kt index 45c55bbc06..81da5a7d02 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/hashtags/EventExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/hashtags/EventExt.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.quartz.nip01Core.tags.hashtags -import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey fun Event.forEachHashTag(onEach: (eventId: HexKey) -> Unit) = tags.forEachHashTag(onEach) 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 edcfc25620..01466ff1b1 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 @@ -20,7 +20,7 @@ */ package com.vitorpamplona.quartz.nip01Core.tags.hashtags -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArray import com.vitorpamplona.quartz.nip01Core.core.anyTagged import com.vitorpamplona.quartz.nip01Core.core.firstAnyLowercaseTaggedValue 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 index ff106533b6..14fa8e1eb9 100644 --- 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 @@ -20,8 +20,8 @@ */ package com.vitorpamplona.quartz.nip01Core.tags.kinds -import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey fun Event.forEachKindTag(onEach: (eventId: HexKey) -> Unit) = tags.forEachKind(onEach) 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 index ac6c053805..c3238ee5de 100644 --- 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 @@ -20,7 +20,7 @@ */ package com.vitorpamplona.quartz.nip01Core.tags.kinds -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArray import com.vitorpamplona.quartz.nip01Core.core.any import com.vitorpamplona.quartz.nip01Core.core.anyTagged 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 index acdbc6266d..9064ff6b0d 100644 --- 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 @@ -22,10 +22,10 @@ package com.vitorpamplona.quartz.nip01Core.tags.people import android.util.Log import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Tag +import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/people/PubKeyReferenceTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/people/PubKeyReferenceTag.kt index 4fb4bdb6dc..7e36107d4a 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/people/PubKeyReferenceTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/people/PubKeyReferenceTag.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.quartz.nip01Core.tags.people -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey interface PubKeyReferenceTag { val pubKey: HexKey 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 index 8e3e7c9ac4..f3728d5da5 100644 --- 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 @@ -20,8 +20,8 @@ */ 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.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder fun TagArrayBuilder.pTag( 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 5364cb0936..8669f730e2 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/ContactListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/ContactListEvent.kt @@ -21,8 +21,8 @@ package com.vitorpamplona.quartz.nip02FollowList import androidx.compose.runtime.Stable -import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/ContactTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/ContactTag.kt index 14b46d196c..d3c26d2ee8 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/ContactTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/ContactTag.kt @@ -22,8 +22,8 @@ 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.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip19Bech32.decodePublicKey import com.vitorpamplona.quartz.utils.arrayOfNotNull import com.vitorpamplona.quartz.utils.bytesUsedInMemory 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 3d55455b92..ab0fc0594b 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/OtsEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/OtsEvent.kt @@ -22,10 +22,10 @@ package com.vitorpamplona.quartz.nip03Timestamp 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.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder -import com.vitorpamplona.quartz.nip01Core.hexToByteArray +import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.events.ETag import com.vitorpamplona.quartz.nip01Core.tags.events.eTag diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/VerificationStateCache.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/VerificationStateCache.kt index 625e993dbf..98d7ce76a0 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/VerificationStateCache.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/VerificationStateCache.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.quartz.nip03Timestamp import android.util.LruCache -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.utils.TimeUtils object VerificationStateCache { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/messages/PrivateDmEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/messages/PrivateDmEvent.kt index 8b2e09b6f5..3e31478a03 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/messages/PrivateDmEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/messages/PrivateDmEvent.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.quartz.nip04Dm.messages 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.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.core.any import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner 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 0714508b59..1249a06e5b 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip09Deletions/DeletionEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip09Deletions/DeletionEvent.kt @@ -21,9 +21,9 @@ package com.vitorpamplona.quartz.nip09Deletions 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.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/BaseThreadedEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/BaseThreadedEvent.kt index bef9ca4b41..5123f0c07d 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/BaseThreadedEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/BaseThreadedEvent.kt @@ -21,8 +21,8 @@ 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.core.HexKey import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedATags import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUsers import com.vitorpamplona.quartz.nip10Notes.content.findIndexTagsWithEventsOrAddresses 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 00bfe41301..ac2f361693 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/TextNoteEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/TextNoteEvent.kt @@ -22,7 +22,7 @@ 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.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip10Notes.tags.markedETags 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 index b4ddaa5e11..1c3ca60652 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/tags/MarkedETag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/tags/MarkedETag.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.quartz.nip10Notes.tags import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.tags.events.GenericETag import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent import com.vitorpamplona.quartz.utils.arrayOfNotNull diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/EventExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/EventExt.kt index a38053a73d..1a7b858a36 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/EventExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/EventExt.kt @@ -21,8 +21,13 @@ package com.vitorpamplona.quartz.nip13Pow import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip13Pow.miner.PoWRankEvaluator -fun Event.pow() = PoWRankProcessor.compute(id, tags.commitedPoW()) +fun Event.pow() = PoWRankEvaluator.compute(id, tags.commitedPoW()) + +fun Event.powNonce() = tags.powTag() + +fun Event.powNonces() = tags.powTags() fun Event.hasPoWTag() = tags.hasPoW() @@ -34,7 +39,7 @@ fun Event.hasPoWTag() = tags.hasPoW() fun Event.strongPoWOrNull(min: Int = 20): Int? { val commitment = tags.commitedPoW() if (commitment != null) { - val pow = PoWRankProcessor.compute(id, commitment) + val pow = PoWRankEvaluator.compute(id, commitment) if (pow >= min) { return pow } 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 5713dd502b..16b41bc660 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/TagArrayBuilderExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/TagArrayBuilderExt.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.quartz.nip13Pow import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip13Pow.tags.PoWTag fun TagArrayBuilder.pow( nonce: String, 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 e0ffb08256..e8e39f507f 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/TagArrayExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/TagArrayExt.kt @@ -21,9 +21,12 @@ package com.vitorpamplona.quartz.nip13Pow import com.vitorpamplona.quartz.nip01Core.core.TagArray +import com.vitorpamplona.quartz.nip13Pow.tags.PoWTag fun TagArray.commitedPoW() = this.firstNotNullOfOrNull(PoWTag::parseCommitment) fun TagArray.hasPoW() = this.any(PoWTag::hasTagWithContent) +fun TagArray.powTag() = this.firstNotNullOfOrNull(PoWTag::parse) + fun TagArray.powTags() = this.mapNotNull(PoWTag::parse) diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip13Pow/PoWRankProcessorTest.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/miner/ByteArrayExt.kt similarity index 55% rename from quartz/src/androidTest/java/com/vitorpamplona/quartz/nip13Pow/PoWRankProcessorTest.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/miner/ByteArrayExt.kt index 863a2c1471..b4dfbc714f 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip13Pow/PoWRankProcessorTest.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/miner/ByteArrayExt.kt @@ -18,32 +18,32 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip13Pow +package com.vitorpamplona.quartz.nip13Pow.miner -import androidx.test.ext.junit.runners.AndroidJUnit4 -import org.junit.Assert.assertEquals -import org.junit.Test -import org.junit.runner.RunWith +/** finds the sequence inside the bytearray */ +fun ByteArray.indexOf(sequence: ByteArray): Int { + if (sequence.isEmpty()) throw IllegalArgumentException("non-empty byte sequence is required") -@RunWith(AndroidJUnit4::class) -class PoWRankProcessorTest { - @Test - fun setPoW() { - assertEquals(26, PoWRankProcessor.calculatePowRankOf("00000026c91e9fc75fdb95b367776e2594b931cebda6d5ca3622501006669c9e")) + var matchOffset = 0 + var start = 0 + + for (offset in 0 until size) { + if (this[offset] == sequence[matchOffset]) { + if (matchOffset++ == 0) start = offset + if (matchOffset == sequence.size) return start + } else { + matchOffset = 0 + } } + return -1 +} - @Test - fun setPoWIfCommited25() { - assertEquals(25, PoWRankProcessor.compute("00000026c91e9fc75fdb95b367776e2594b931cebda6d5ca3622501006669c9e", 25)) - } - - @Test - fun setPoWIfCommited26() { - assertEquals(26, PoWRankProcessor.compute("00000026c91e9fc75fdb95b367776e2594b931cebda6d5ca3622501006669c9e", 26)) - } - - @Test - fun setPoWIfCommited27() { - assertEquals(26, PoWRankProcessor.compute("00000026c91e9fc75fdb95b367776e2594b931cebda6d5ca3622501006669c9e", 27)) +fun ByteArray.set( + value: ByteArray, + startIndex: Int, +) { + var index = startIndex + for (byte in value) { + this[index++] = byte } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/miner/MiningBuffer.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/miner/MiningBuffer.kt new file mode 100644 index 0000000000..ac5135e57f --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/miner/MiningBuffer.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.nip13Pow.miner + +class MiningBuffer( + val bytes: ByteArray, + val nonceStarts: Int, + val nonceEnds: Int, +) { + fun nonce() = String(bytes.copyOfRange(nonceStarts, nonceEnds)) +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/miner/PoWMiner.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/miner/PoWMiner.kt new file mode 100644 index 0000000000..3169e49e6e --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/miner/PoWMiner.kt @@ -0,0 +1,106 @@ +/** + * 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.nip13Pow.miner + +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.crypto.EventHasher +import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate +import com.vitorpamplona.quartz.nip13Pow.tags.PoWTag +import com.vitorpamplona.quartz.utils.sha256.Sha256Hasher + +class PoWMiner( + val buffer: MiningBuffer, + val desiredPoW: Int, +) { + val hasher = Sha256Hasher() + + fun rank(byteArray: ByteArray) = PoWRankEvaluator.calculatePowRankOf(hasher.hash(byteArray)) + + fun run() = runDigit(buffer.nonceStarts) + + private fun runDigit(index: Int): Boolean { + for (testByte in VALID_BYTES) { + // replaces the background base by the nonce integers + buffer.bytes[index] = testByte + + if (rank(buffer.bytes) >= desiredPoW) return true + + if (index + 1 < buffer.nonceEnds && runDigit(index + 1)) return true + } + return false + } + + companion object { + private val STARTING_NONCE_SIZE = 5 + + // make sure these chars are not escaped by the JSON stringifier + private val VALID_CHARS: List = + ('0'..'9') + ('a'..'z') + ('A'..'Z') + "-()[]{}$@!*=;:?,".toCharArray().toList() + + private val VALID_BYTES = VALID_CHARS.map { it.code.toByte() } + + private fun randomBase(size: Int): String = CharArray(size) { VALID_CHARS.random() }.concatToString() + + /** + * The miner creates a stringified json template and changes the nonce directly in the UTF-8 ByteArray representation + * to avoid having to recompute the json objects and stringify it. + */ + fun run( + template: EventTemplate, + pubKey: HexKey, + desiredPoW: Int, + ): EventTemplate { + var nextSize = STARTING_NONCE_SIZE + + do { + val initialNonce = randomBase(nextSize) + + val bytes = + EventHasher + .makeJsonForId( + pubKey, + template.createdAt, + template.kind, + template.tags + PoWTag.assemble(initialNonce, desiredPoW), + template.content, + ).toByteArray() + + val startIndex = bytes.indexOf(initialNonce.toByteArray()) + + val buffer = MiningBuffer(bytes, startIndex, startIndex + nextSize) + + if (PoWMiner(buffer, desiredPoW).run()) { + return EventTemplate( + template.createdAt, + template.kind, + template.tags + PoWTag.assemble(buffer.nonce(), desiredPoW), + template.content, + ) + } else { + nextSize += STARTING_NONCE_SIZE + } + } while (nextSize < 50) + + throw RuntimeException("Could not find PoW") + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/PoWRankProcessor.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/miner/PoWRankEvaluator.kt similarity index 60% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/PoWRankProcessor.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/miner/PoWRankEvaluator.kt index f47e1c0b4d..0a65d591f7 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/PoWRankProcessor.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/miner/PoWRankEvaluator.kt @@ -18,12 +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.nip13Pow +package com.vitorpamplona.quartz.nip13Pow.miner -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey -class PoWRankProcessor { +class PoWRankEvaluator { companion object { + const val R8 = 0b00000000.toByte() + const val R7 = 0b00000001.toByte() + const val R6 = 0b00000010.toByte() + const val R5 = 0b00000100.toByte() + const val R4 = 0b00001000.toByte() + const val R3 = 0b00010000.toByte() + const val R2 = 0b00100000.toByte() + const val R1 = 0b01000000.toByte() + const val NEGATIVE = 0b10000000.toByte() + @JvmStatic fun compute( id: HexKey, @@ -63,5 +73,35 @@ class PoWRankProcessor { } return rank } + + @JvmStatic + fun calculatePowRankOf(id: ByteArray): Int { + var rank = 0 + for (byte in id) { + if (byte == R8) { + rank += 8 + } else if (byte < 0) { + break + } else { + if (byte < R6) { + rank += 7 + } else if (byte < R5) { + rank += 6 + } else if (byte < R4) { + rank += 5 + } else if (byte < R3) { + rank += 4 + } else if (byte < R2) { + rank += 3 + } else if (byte < R1) { + rank += 2 + } else { + rank += 1 + } + break + } + } + return rank + } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/PoWTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/tags/PoWTag.kt similarity index 98% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/PoWTag.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/tags/PoWTag.kt index fd372c1226..8c2fd55107 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/PoWTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/tags/PoWTag.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.nip13Pow +package com.vitorpamplona.quartz.nip13Pow.tags import com.vitorpamplona.quartz.utils.arrayOfNotNull import com.vitorpamplona.quartz.utils.bytesUsedInMemory 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 ac156bb894..d4f6dc232a 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/NIP17Factory.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/NIP17Factory.kt @@ -21,8 +21,8 @@ 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.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent 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 index 8ec33b140c..b12f689f35 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/base/BaseDMGroupEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/base/BaseDMGroupEvent.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.quartz.nip17Dm.base import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip59Giftwrap.WrappedEvent import kotlinx.collections.immutable.toImmutableSet diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/base/ChatroomKey.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/base/ChatroomKey.kt index 4d6da3fe0b..7a799c81fb 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/base/ChatroomKey.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/base/ChatroomKey.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.quartz.nip17Dm.base import androidx.compose.runtime.Stable -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey @Stable data class ChatroomKey( diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/base/ChatroomKeyable.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/base/ChatroomKeyable.kt index ad92949f3f..b82ac3684b 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/base/ChatroomKeyable.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/base/ChatroomKeyable.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.quartz.nip17Dm.base -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey interface ChatroomKeyable { fun chatroomKey(toRemove: HexKey): ChatroomKey diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/base/NIP17Group.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/base/NIP17Group.kt index a422a3ee08..07dd6f3d73 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/base/NIP17Group.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/base/NIP17Group.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.quartz.nip17Dm.base -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey interface NIP17Group { fun groupMembers(): Set 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 index 35fda32418..ea04eefcac 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/files/ChatMessageEncryptedFileHeaderEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/files/ChatMessageEncryptedFileHeaderEvent.kt @@ -22,7 +22,7 @@ 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.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.events.ETag 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 index 4f003cffac..e875d8ab4f 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/files/TagArrayBuilderExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/files/TagArrayBuilderExt.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.quartz.nip17Dm.files import com.vitorpamplona.quartz.nip01Core.EventHintBundle -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip01Core.tags.people.pTag diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/files/encryption/AESGCM.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/files/encryption/AESGCM.kt index 47c3295b84..9007b5bacc 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/files/encryption/AESGCM.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/files/encryption/AESGCM.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.quartz.nip17Dm.files.encryption -import com.vitorpamplona.quartz.nip01Core.toHexKey +import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.utils.RandomInstance import javax.crypto.Cipher import javax.crypto.spec.GCMParameterSpec 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 index 4b57b1bf99..d3ed47c411 100644 --- 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 @@ -20,8 +20,8 @@ */ package com.vitorpamplona.quartz.nip17Dm.files.tags -import com.vitorpamplona.quartz.nip01Core.hexToByteArray -import com.vitorpamplona.quartz.nip01Core.toHexKey +import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray +import com.vitorpamplona.quartz.nip01Core.core.toHexKey class EncryptionKey { companion object { 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 index 41c051ad15..6abc59ce9c 100644 --- 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 @@ -20,8 +20,8 @@ */ package com.vitorpamplona.quartz.nip17Dm.files.tags -import com.vitorpamplona.quartz.nip01Core.hexToByteArray -import com.vitorpamplona.quartz.nip01Core.toHexKey +import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray +import com.vitorpamplona.quartz.nip01Core.core.toHexKey class EncryptionNonce { companion object { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/messages/ChatMessageEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/messages/ChatMessageEvent.kt index 680d0ab30c..a6802b3646 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/messages/ChatMessageEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/messages/ChatMessageEvent.kt @@ -22,7 +22,7 @@ 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.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.events.ETag diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/settings/ChatMessageRelayListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/settings/ChatMessageRelayListEvent.kt index c48500e6f3..300c2b19d8 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/settings/ChatMessageRelayListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/settings/ChatMessageRelayListEvent.kt @@ -21,8 +21,8 @@ package com.vitorpamplona.quartz.nip17Dm.settings import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.BaseReplaceableEvent +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag 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 08e331d574..b392485b2c 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/GenericRepostEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/GenericRepostEvent.kt @@ -21,9 +21,9 @@ package com.vitorpamplona.quartz.nip18Reposts 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.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate 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 db25dc36d4..97efab8013 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/RepostEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/RepostEvent.kt @@ -21,9 +21,9 @@ package com.vitorpamplona.quartz.nip18Reposts 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.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.addressables.aTag 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 index 2f7169c7e4..f499b58069 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/EventExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/EventExt.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.quartz.nip18Reposts.quotes -import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey fun Event.forEachTaggedQuoteId(onEach: (eventId: HexKey) -> Unit) = tags.forEachTaggedQuoteId(onEach) 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 index f4ddd5bd99..fa374d475c 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/QAddressableTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/QAddressableTag.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.quartz.nip18Reposts.quotes import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.utils.arrayOfNotNull import com.vitorpamplona.quartz.utils.bytesUsedInMemory 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 index 864eaf14a1..511bb12d0f 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/QEventTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/QEventTag.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.quartz.nip18Reposts.quotes import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.utils.arrayOfNotNull import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.pointerSizeInBytes 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 index bd0d2c1935..3b9163da7e 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/TagArrayExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/TagArrayExt.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.quartz.nip18Reposts.quotes -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArray import com.vitorpamplona.quartz.nip01Core.core.forEachTagged import com.vitorpamplona.quartz.nip01Core.core.isTagged 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 697269977b..7d1cceff91 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/Nip19Parser.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/Nip19Parser.kt @@ -22,10 +22,10 @@ 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.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray +import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.crypto.Nip01 -import com.vitorpamplona.quartz.nip01Core.hexToByteArray -import com.vitorpamplona.quartz.nip01Core.toHexKey import com.vitorpamplona.quartz.nip19Bech32.bech32.bechToBytes import com.vitorpamplona.quartz.nip19Bech32.entities.Entity import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/TlvBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/TlvBuilderExt.kt index dd4dd160f3..d929089106 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/TlvBuilderExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/TlvBuilderExt.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. */ -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip19Bech32.TlvTypes import com.vitorpamplona.quartz.nip19Bech32.tlv.TlvBuilder diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/entities/NPub.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/entities/NPub.kt index d475c37f27..f9b8fba128 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/entities/NPub.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/entities/NPub.kt @@ -21,9 +21,9 @@ package com.vitorpamplona.quartz.nip19Bech32.entities import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey -import com.vitorpamplona.quartz.nip01Core.hexToByteArray -import com.vitorpamplona.quartz.nip01Core.toHexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray +import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip19Bech32.toNpub @Immutable diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/entities/NSec.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/entities/NSec.kt index 7a18c3fd67..09f81b0b00 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/entities/NSec.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/entities/NSec.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.quartz.nip19Bech32.entities import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.toHexKey +import com.vitorpamplona.quartz.nip01Core.core.toHexKey @Immutable data class NSec( diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/entities/Note.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/entities/Note.kt index fa533f42f9..e52bb0ddc6 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/entities/Note.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/entities/Note.kt @@ -21,9 +21,9 @@ package com.vitorpamplona.quartz.nip19Bech32.entities import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey -import com.vitorpamplona.quartz.nip01Core.hexToByteArray -import com.vitorpamplona.quartz.nip01Core.toHexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray +import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip19Bech32.toNote @Immutable diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/tlv/Tlv.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/tlv/Tlv.kt index d99bc59f32..27a4b703c3 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/tlv/Tlv.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/tlv/Tlv.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.quartz.nip19Bech32.tlv -import com.vitorpamplona.quartz.nip01Core.toHexKey +import com.vitorpamplona.quartz.nip01Core.core.toHexKey import java.nio.ByteBuffer import java.nio.ByteOrder diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/tlv/TlvBuilder.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/tlv/TlvBuilder.kt index 08f2519d9e..1c6efb71dd 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/tlv/TlvBuilder.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/tlv/TlvBuilder.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.quartz.nip19Bech32.tlv -import com.vitorpamplona.quartz.nip01Core.HexKey -import com.vitorpamplona.quartz.nip01Core.hexToByteArray +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray import java.io.ByteArrayOutputStream class TlvBuilder { 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 bc62dec532..f35d88fbae 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/CommentEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/CommentEvent.kt @@ -22,9 +22,9 @@ package com.vitorpamplona.quartz.nip22Comments 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.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/TagArrayBuilderExt.kt index a5b5d2ebe4..3592427787 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/TagArrayBuilderExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/TagArrayBuilderExt.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.quartz.nip22Comments -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip01Core.tags.people.pTag 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 index e37b6afbc7..7550a5e881 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/ReplyAddressTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/ReplyAddressTag.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.quartz.nip22Comments.tags import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Tag import com.vitorpamplona.quartz.nip01Core.core.match import com.vitorpamplona.quartz.nip01Core.core.valueIfMatches 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 index 0b42d207b3..48b6b1ade1 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/ReplyAuthorTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/ReplyAuthorTag.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.quartz.nip22Comments.tags import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Tag import com.vitorpamplona.quartz.nip01Core.core.match import com.vitorpamplona.quartz.nip01Core.tags.people.PubKeyReferenceTag 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 index 26926372fb..ac4c9da01f 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/ReplyEventTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/ReplyEventTag.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.quartz.nip22Comments.tags import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Tag import com.vitorpamplona.quartz.nip01Core.core.match import com.vitorpamplona.quartz.nip01Core.core.valueIfMatches 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 index e4dbbc220d..c46dcf8651 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/RootAddressTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/RootAddressTag.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.quartz.nip22Comments.tags import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Tag import com.vitorpamplona.quartz.nip01Core.core.match import com.vitorpamplona.quartz.nip01Core.core.valueIfMatches 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 index 5b6d44c6f4..a24430433e 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/RootAuthorTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/RootAuthorTag.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.quartz.nip22Comments.tags import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Tag import com.vitorpamplona.quartz.nip01Core.core.match import com.vitorpamplona.quartz.nip01Core.tags.people.PubKeyReferenceTag 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 index c26282cc4f..b15c2401e4 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/RootEventTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/RootEventTag.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.quartz.nip22Comments.tags import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Tag import com.vitorpamplona.quartz.nip01Core.core.match import com.vitorpamplona.quartz.nip01Core.core.valueIfMatches 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 1729b0d843..f5195f4649 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip23LongContent/LongTextNoteEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip23LongContent/LongTextNoteEvent.kt @@ -21,8 +21,8 @@ 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.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag 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 2b7ef491f3..0cc88e96a0 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip25Reactions/ReactionEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip25Reactions/ReactionEvent.kt @@ -22,9 +22,9 @@ 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.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.addressables.aTag import com.vitorpamplona.quartz.nip01Core.tags.events.ETag 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 14c89f45cb..e892b6f8d0 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/ChannelListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/ChannelListEvent.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.quartz.nip28PublicChat import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.nip51Lists.GeneralListEvent diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelCreateEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelCreateEvent.kt index 2779fa7112..1b1e015e05 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelCreateEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelCreateEvent.kt @@ -21,8 +21,8 @@ package com.vitorpamplona.quartz.nip28PublicChat.admin import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip28PublicChat.base.ChannelData diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelHideMessageEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelHideMessageEvent.kt index d8b2b2ad25..69827dab85 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelHideMessageEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelHideMessageEvent.kt @@ -22,7 +22,7 @@ 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.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.events.ETag 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 index a34f2d39aa..e37e0ce5c8 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelMetadataEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelMetadataEvent.kt @@ -22,7 +22,7 @@ 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.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.events.ETag diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelMuteUserEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelMuteUserEvent.kt index 66b051ed5a..3f97d28f73 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelMuteUserEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelMuteUserEvent.kt @@ -22,7 +22,7 @@ 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.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.people.PTag 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 index 8857508ed7..4a76981fa4 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/base/BasePublicChatEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/base/BasePublicChatEvent.kt @@ -21,8 +21,8 @@ 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.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag @Immutable 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 index 1d872a0e3c..f5947024d3 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/message/ChannelMessageEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/message/ChannelMessageEvent.kt @@ -22,7 +22,7 @@ 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.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.events.ETag diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip30CustomEmoji/pack/EmojiPackEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip30CustomEmoji/pack/EmojiPackEvent.kt index e0daf3ac68..a14048f316 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip30CustomEmoji/pack/EmojiPackEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip30CustomEmoji/pack/EmojiPackEvent.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.quartz.nip30CustomEmoji.pack import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag 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 index 0d5a5a6bcb..7bfe405353 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip30CustomEmoji/selection/EmojiPackSelectionEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip30CustomEmoji/selection/EmojiPackSelectionEvent.kt @@ -22,8 +22,8 @@ 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.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.signers.eventUpdate diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/issue/GitIssueEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/issue/GitIssueEvent.kt index 2ca47f1382..e17c49ecaa 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/issue/GitIssueEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/issue/GitIssueEvent.kt @@ -22,7 +22,7 @@ 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.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/patch/GitPatchEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/patch/GitPatchEvent.kt index f7bfa67500..d0a91b5b20 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/patch/GitPatchEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/patch/GitPatchEvent.kt @@ -21,8 +21,8 @@ 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.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address 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 index 538a4e51fd..e65622051a 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/reply/GitReplyEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/reply/GitReplyEvent.kt @@ -22,7 +22,7 @@ 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.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/repository/GitRepositoryEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/repository/GitRepositoryEvent.kt index 857b95be0d..be5f5416f7 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/repository/GitRepositoryEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/repository/GitRepositoryEvent.kt @@ -21,8 +21,8 @@ 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.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag 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 fad34983c4..12f83a3895 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip35Torrents/TorrentCommentEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip35Torrents/TorrentCommentEvent.kt @@ -22,7 +22,7 @@ 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.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate 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 cf5c42caa4..6d7532c4d9 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip35Torrents/TorrentEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip35Torrents/TorrentEvent.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.quartz.nip35Torrents 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.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags 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 11156dafce..a0de590fc2 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip37Drafts/DraftEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip37Drafts/DraftEvent.kt @@ -23,9 +23,9 @@ package com.vitorpamplona.quartz.nip37Drafts import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryBaseEvent import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent -import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip38UserStatus/StatusEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip38UserStatus/StatusEvent.kt index 196bf1b08b..4ee1ba535b 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip38UserStatus/StatusEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip38UserStatus/StatusEvent.kt @@ -21,8 +21,8 @@ package com.vitorpamplona.quartz.nip38UserStatus import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.firstTagValue import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.utils.TimeUtils diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip42RelayAuth/RelayAuthEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip42RelayAuth/RelayAuthEvent.kt index c835c05026..9cf8af432b 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip42RelayAuth/RelayAuthEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip42RelayAuth/RelayAuthEvent.kt @@ -21,8 +21,8 @@ package com.vitorpamplona.quartz.nip42RelayAuth import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.utils.TimeUtils diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip44Encryption/Nip44v2.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip44Encryption/Nip44v2.kt index 5544888a1a..f4843d403d 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip44Encryption/Nip44v2.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip44Encryption/Nip44v2.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.quartz.nip44Encryption import android.util.Log -import com.vitorpamplona.quartz.nip01Core.toHexKey +import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip44Encryption.crypto.Hkdf import com.vitorpamplona.quartz.utils.LibSodiumInstance import com.vitorpamplona.quartz.utils.RandomInstance diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip46RemoteSigner/BunkerRequestConnect.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip46RemoteSigner/BunkerRequestConnect.kt index e2b41c7e60..a65c6f4f17 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip46RemoteSigner/BunkerRequestConnect.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip46RemoteSigner/BunkerRequestConnect.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.quartz.nip46RemoteSigner -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey import java.util.UUID class BunkerRequestConnect( diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip46RemoteSigner/BunkerRequestNip04Decrypt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip46RemoteSigner/BunkerRequestNip04Decrypt.kt index 20bf478a65..33923c9c00 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip46RemoteSigner/BunkerRequestNip04Decrypt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip46RemoteSigner/BunkerRequestNip04Decrypt.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.quartz.nip46RemoteSigner -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey import java.util.UUID class BunkerRequestNip04Decrypt( diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip46RemoteSigner/BunkerRequestNip04Encrypt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip46RemoteSigner/BunkerRequestNip04Encrypt.kt index ee41077309..71d2a6f7e5 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip46RemoteSigner/BunkerRequestNip04Encrypt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip46RemoteSigner/BunkerRequestNip04Encrypt.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.quartz.nip46RemoteSigner -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey import java.util.UUID class BunkerRequestNip04Encrypt( diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip46RemoteSigner/BunkerRequestNip44Decrypt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip46RemoteSigner/BunkerRequestNip44Decrypt.kt index 0769163c53..f4a7e711a5 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip46RemoteSigner/BunkerRequestNip44Decrypt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip46RemoteSigner/BunkerRequestNip44Decrypt.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.quartz.nip46RemoteSigner -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey import java.util.UUID class BunkerRequestNip44Decrypt( diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip46RemoteSigner/BunkerRequestNip44Encrypt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip46RemoteSigner/BunkerRequestNip44Encrypt.kt index 8f7064808a..cff4d36f67 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip46RemoteSigner/BunkerRequestNip44Encrypt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip46RemoteSigner/BunkerRequestNip44Encrypt.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.quartz.nip46RemoteSigner -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey import java.util.UUID class BunkerRequestNip44Encrypt( diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip46RemoteSigner/BunkerResponsePublicKey.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip46RemoteSigner/BunkerResponsePublicKey.kt index bbd4a99692..4fc04c5d18 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip46RemoteSigner/BunkerResponsePublicKey.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip46RemoteSigner/BunkerResponsePublicKey.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.quartz.nip46RemoteSigner -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey import java.util.UUID class BunkerResponsePublicKey( 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 bf4b55b548..b15b53830b 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip46RemoteSigner/NostrConnectEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip46RemoteSigner/NostrConnectEvent.kt @@ -21,8 +21,8 @@ package com.vitorpamplona.quartz.nip46RemoteSigner import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip31Alts.AltTag 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 854250f2ec..0b4676420a 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentRequestEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentRequestEvent.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.quartz.nip47WalletConnect 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.core.HexKey import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip31Alts.AltTag diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentResponseEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentResponseEvent.kt index a033e25cc5..87f5a3e888 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentResponseEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentResponseEvent.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.quartz.nip47WalletConnect 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.core.HexKey import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.utils.pointerSizeInBytes diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip47WalletConnect/Nip47WalletConnect.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip47WalletConnect/Nip47WalletConnect.kt index 815a479f36..a9f52689c8 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip47WalletConnect/Nip47WalletConnect.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip47WalletConnect/Nip47WalletConnect.kt @@ -21,8 +21,8 @@ package com.vitorpamplona.quartz.nip47WalletConnect import android.net.Uri -import com.vitorpamplona.quartz.nip01Core.HexKey -import com.vitorpamplona.quartz.nip01Core.toHexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip19Bech32.decodePublicKey import kotlinx.coroutines.CancellationException diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip49PrivKeyEnc/Nip49.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip49PrivKeyEnc/Nip49.kt index 04fc996592..b3f0575aad 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip49PrivKeyEnc/Nip49.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip49PrivKeyEnc/Nip49.kt @@ -21,9 +21,9 @@ package com.vitorpamplona.quartz.nip49PrivKeyEnc import android.util.Log -import com.vitorpamplona.quartz.nip01Core.HexKey -import com.vitorpamplona.quartz.nip01Core.hexToByteArray -import com.vitorpamplona.quartz.nip01Core.toHexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray +import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip19Bech32.bech32.bechToBytes import com.vitorpamplona.quartz.utils.LibSodiumInstance import com.vitorpamplona.quartz.utils.RandomInstance 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 088057ef75..d439328699 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip50Search/SearchRelayListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip50Search/SearchRelayListEvent.kt @@ -21,8 +21,8 @@ package com.vitorpamplona.quartz.nip50Search import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.BaseReplaceableEvent +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag 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 2fad460c85..75070bb96e 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/BookmarkListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/BookmarkListEvent.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.quartz.nip51Lists import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip31Alts.AltTag 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 ef8aec468a..1f1f16f51b 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/GeneralListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/GeneralListEvent.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.quartz.nip51Lists import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.isTagged import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner 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 8c1c589f8e..1148fb1b8f 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/MuteListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/MuteListEvent.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.quartz.nip51Lists import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip31Alts.AltTag 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 8b03f1ec7e..1fa5058e5a 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/PeopleListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/PeopleListEvent.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.quartz.nip51Lists import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip31Alts.AltTag 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 1540c10480..d993c3fcc5 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/PinListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/PinListEvent.kt @@ -21,8 +21,8 @@ package com.vitorpamplona.quartz.nip51Lists import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.utils.TimeUtils 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 5dbe05ff71..cf8ea9147d 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/PrivateTagArrayEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/PrivateTagArrayEvent.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.quartz.nip51Lists import android.util.Log import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.pointerSizeInBytes 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 59166d9110..49e9aae885 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/RelaySetEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/RelaySetEvent.kt @@ -21,8 +21,8 @@ package com.vitorpamplona.quartz.nip51Lists import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.utils.TimeUtils 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 77d97fa421..29a5210d95 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip52Calendar/CalendarDateSlotEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip52Calendar/CalendarDateSlotEvent.kt @@ -21,8 +21,8 @@ package com.vitorpamplona.quartz.nip52Calendar import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.firstTagValue import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip31Alts.AltTag 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 0bcfd8bcca..8516598156 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip52Calendar/CalendarEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip52Calendar/CalendarEvent.kt @@ -21,8 +21,8 @@ package com.vitorpamplona.quartz.nip52Calendar import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.utils.TimeUtils 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 0be0ed45ec..cc0e8f3bcc 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip52Calendar/CalendarRSVPEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip52Calendar/CalendarRSVPEvent.kt @@ -21,8 +21,8 @@ package com.vitorpamplona.quartz.nip52Calendar import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.firstTagValue import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip31Alts.AltTag 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 887f8bbdf8..db2621c21d 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip52Calendar/CalendarTimeSlotEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip52Calendar/CalendarTimeSlotEvent.kt @@ -21,8 +21,8 @@ package com.vitorpamplona.quartz.nip52Calendar import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.firstTagValue import com.vitorpamplona.quartz.nip01Core.core.firstTagValueAsLong import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner 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 index ebf5926dc4..6162821532 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/chat/LiveActivitiesChatMessageEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/chat/LiveActivitiesChatMessageEvent.kt @@ -22,7 +22,7 @@ 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.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag 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 index 19dd12bf1d..b94e2a0cd8 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/LiveActivitiesEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/LiveActivitiesEvent.kt @@ -21,8 +21,8 @@ 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.HexKey import com.vitorpamplona.quartz.nip01Core.core.any import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip23LongContent.tags.ImageTag 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 index 565f5c02ec..a77348d0d7 100644 --- 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 @@ -21,7 +21,7 @@ package com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Tag import com.vitorpamplona.quartz.nip01Core.core.isNotName import com.vitorpamplona.quartz.nip01Core.tags.people.PubKeyReferenceTag 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 2b3b27b457..abb9192f85 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip54Wiki/WikiNoteEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip54Wiki/WikiNoteEvent.kt @@ -21,8 +21,8 @@ package com.vitorpamplona.quartz.nip54Wiki import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/ExternalSignerLauncher.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/ExternalSignerLauncher.kt index 35f86d37ca..f375dad427 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/ExternalSignerLauncher.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/ExternalSignerLauncher.kt @@ -34,8 +34,8 @@ import com.fasterxml.jackson.databind.deser.std.StdDeserializer import com.fasterxml.jackson.databind.module.SimpleModule import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper 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.core.HexKey import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent enum class SignerType { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/NostrSignerExternal.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/NostrSignerExternal.kt index 7ee5a51ad4..b1e5bf32ec 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/NostrSignerExternal.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/NostrSignerExternal.kt @@ -22,9 +22,9 @@ package com.vitorpamplona.quartz.nip55AndroidSigner import android.util.Log 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.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.crypto.EventHasher import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent 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 4d95a7a2e0..3e222faab3 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip56Reports/ReportEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip56Reports/ReportEvent.kt @@ -21,9 +21,9 @@ package com.vitorpamplona.quartz.nip56Reports 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.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.utils.TimeUtils 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 172f661211..a8855e4b76 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapEvent.kt @@ -24,8 +24,8 @@ import android.util.Log import androidx.compose.runtime.Immutable 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 +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.utils.pointerSizeInBytes @Immutable diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapPrivateEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapPrivateEvent.kt index bf95cad914..3b0aceb66a 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapPrivateEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapPrivateEvent.kt @@ -21,8 +21,8 @@ package com.vitorpamplona.quartz.nip57Zaps import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync import com.vitorpamplona.quartz.utils.TimeUtils 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 38bed9b9eb..b4e6cf173b 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapRequestEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapRequestEvent.kt @@ -22,12 +22,12 @@ package com.vitorpamplona.quartz.nip57Zaps import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.experimental.zapPolls.tags.PollOptionTag -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.HexKey +import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray 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.AltTag 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 0b0a8b9f95..d71c73bc09 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/PrivateZapRequestBuilder.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/PrivateZapRequestBuilder.kt @@ -21,11 +21,11 @@ package com.vitorpamplona.quartz.nip57Zaps import android.util.Log +import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray +import com.vitorpamplona.quartz.nip01Core.core.toHexKey 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.signers.NostrSignerSync -import com.vitorpamplona.quartz.nip01Core.toHexKey class PrivateZapRequestBuilder { fun signPrivateZapRequest( diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/splits/ZapSplitSetup.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/splits/ZapSplitSetup.kt index 1455ebefc4..3988dfb60b 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/splits/ZapSplitSetup.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/splits/ZapSplitSetup.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.quartz.nip57Zaps.splits -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey data class ZapSplitSetup( val pubKeyHex: HexKey, 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 101f965ab1..b7074d7ebd 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip58Badges/BadgeAwardEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip58Badges/BadgeAwardEvent.kt @@ -21,8 +21,8 @@ package com.vitorpamplona.quartz.nip58Badges import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedAddresses import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUserIds import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUsers diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip58Badges/BadgeDefinitionEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip58Badges/BadgeDefinitionEvent.kt index bef455d22f..a73e426044 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip58Badges/BadgeDefinitionEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip58Badges/BadgeDefinitionEvent.kt @@ -21,8 +21,8 @@ package com.vitorpamplona.quartz.nip58Badges import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent +import com.vitorpamplona.quartz.nip01Core.core.HexKey @Immutable class BadgeDefinitionEvent( diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip58Badges/BadgeProfilesEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip58Badges/BadgeProfilesEvent.kt index 8493516a53..bdf3900419 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip58Badges/BadgeProfilesEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip58Badges/BadgeProfilesEvent.kt @@ -21,8 +21,8 @@ package com.vitorpamplona.quartz.nip58Badges import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedAddresses diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/HostStub.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/HostStub.kt index 2cf83aa1bc..dcef4c57f0 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/HostStub.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/HostStub.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.quartz.nip59Giftwrap -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey class HostStub( val id: HexKey, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/WrappedEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/WrappedEvent.kt index 1c653a447b..facc1065b3 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/WrappedEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/WrappedEvent.kt @@ -21,8 +21,8 @@ package com.vitorpamplona.quartz.nip59Giftwrap import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey @Immutable open class WrappedEvent( diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/rumors/Rumor.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/rumors/Rumor.kt index 6202fb99e3..ccbec1c13f 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/rumors/Rumor.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/rumors/Rumor.kt @@ -22,9 +22,9 @@ package com.vitorpamplona.quartz.nip59Giftwrap.rumors import com.fasterxml.jackson.annotation.JsonProperty 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.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.crypto.EventHasher import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/seals/SealedRumorEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/seals/SealedRumorEvent.kt index 2fec9cbf39..a1b7c9dd27 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/seals/SealedRumorEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/seals/SealedRumorEvent.kt @@ -22,8 +22,8 @@ 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.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip59Giftwrap.HostStub import com.vitorpamplona.quartz.nip59Giftwrap.WrappedEvent diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/wraps/GiftWrapEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/wraps/GiftWrapEvent.kt index b512fa852a..321ba29e12 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/wraps/GiftWrapEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/wraps/GiftWrapEvent.kt @@ -22,8 +22,8 @@ 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.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.firstTagValue import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner 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 d38259ecff..928ad17749 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip65RelayList/AdvertisedRelayListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip65RelayList/AdvertisedRelayListEvent.kt @@ -21,8 +21,8 @@ package com.vitorpamplona.quartz.nip65RelayList import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.BaseReplaceableEvent +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip65RelayList/RelayListRecommendationProcessor.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip65RelayList/RelayListRecommendationProcessor.kt index 2566987ef2..a87ab59f9b 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip65RelayList/RelayListRecommendationProcessor.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip65RelayList/RelayListRecommendationProcessor.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.quartz.nip65RelayList -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey class RelayListRecommendationProcessor { companion object { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip68Picture/IMetaTagBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip68Picture/IMetaTagBuilderExt.kt index c6a4e421ee..1ab4d044ca 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip68Picture/IMetaTagBuilderExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip68Picture/IMetaTagBuilderExt.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.quartz.nip68Picture -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.nip36SensitiveContent.ContentWarningTag import com.vitorpamplona.quartz.nip68Picture.tags.UserAnnotationTag 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 8aecc2bd9a..bfa141b93e 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip68Picture/PictureEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip68Picture/PictureEvent.kt @@ -21,8 +21,8 @@ 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.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.core.any import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate 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 index c257825fa1..704da38162 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip68Picture/tags/UserAnnotationTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip68Picture/tags/UserAnnotationTag.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.quartz.nip68Picture.tags -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey class UserAnnotationTag( val pubkey: HexKey, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/IMetaTagBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/IMetaTagBuilderExt.kt index 1822b8517a..f8bbe904b0 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/IMetaTagBuilderExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/IMetaTagBuilderExt.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.quartz.nip71Video -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.nip36SensitiveContent.ContentWarningTag import com.vitorpamplona.quartz.nip92IMeta.IMetaTagBuilder 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 a17fd744c1..b2790e9b92 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/VideoEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/VideoEvent.kt @@ -21,8 +21,8 @@ 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.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.any import com.vitorpamplona.quartz.nip01Core.tags.events.ETag import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags 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 f39c53bc9f..905deb4439 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/VideoHorizontalEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/VideoHorizontalEvent.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.quartz.nip71Video import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag 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 58c808f495..8b8ccf616a 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/VideoVerticalEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/VideoVerticalEvent.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.quartz.nip71Video import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/tags/TextTrackTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/tags/TextTrackTag.kt index 645949e74a..1f0680f461 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/tags/TextTrackTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/tags/TextTrackTag.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.quartz.nip71Video.tags -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.utils.arrayOfNotNull import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.pointerSizeInBytes 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 afdf59ba34..6035cae274 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/CommunityListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/CommunityListEvent.kt @@ -21,8 +21,8 @@ package com.vitorpamplona.quartz.nip72ModCommunities 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.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip19Bech32.parseAtag diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/approval/CommunityPostApprovalEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/approval/CommunityPostApprovalEvent.kt index 049843e0bb..c888bc4e83 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/approval/CommunityPostApprovalEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/approval/CommunityPostApprovalEvent.kt @@ -23,8 +23,8 @@ 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.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedATags 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 index bb57e68222..58c548b2bc 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/definition/CommunityDefinitionEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/definition/CommunityDefinitionEvent.kt @@ -21,8 +21,8 @@ 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.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag 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 index f6c6db6b0e..08d901cc2d 100644 --- 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 @@ -21,7 +21,7 @@ package com.vitorpamplona.quartz.nip72ModCommunities.definition.tags import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Tag import com.vitorpamplona.quartz.nip01Core.core.isNotName import com.vitorpamplona.quartz.nip01Core.tags.people.PubKeyReferenceTag 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 a8cb100a52..cfbf375ef2 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip75ZapGoals/GoalEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip75ZapGoals/GoalEvent.kt @@ -22,10 +22,10 @@ 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.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags 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 e02f6e034f..1f543dd3de 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip78AppData/AppSpecificDataEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip78AppData/AppSpecificDataEvent.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.quartz.nip78AppData -import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address 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 9f8bcc63c2..f8ba732469 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip84Highlights/HighlightEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip84Highlights/HighlightEvent.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.quartz.nip84Highlights import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.addressables.firstTaggedATag import com.vitorpamplona.quartz.nip01Core.tags.addressables.firstTaggedAddress diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/definition/AppDefinitionEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/definition/AppDefinitionEvent.kt index bc365793ef..d1836cfe7b 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/definition/AppDefinitionEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/definition/AppDefinitionEvent.kt @@ -22,8 +22,8 @@ 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.core.BaseAddressableEvent +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/recommendation/AppRecommendationEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/recommendation/AppRecommendationEvent.kt index 70737bb422..0b3db29b59 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/recommendation/AppRecommendationEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/recommendation/AppRecommendationEvent.kt @@ -21,8 +21,8 @@ package com.vitorpamplona.quartz.nip89AppHandlers.recommendation import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag 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 6335f4c70b..565f9498ec 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip90Dvms/NIP90ContentDiscoveryRequestEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip90Dvms/NIP90ContentDiscoveryRequestEvent.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.quartz.nip90Dvms import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable -import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.utils.TimeUtils 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 fe1e93ac20..9d182402e7 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip90Dvms/NIP90ContentDiscoveryResponseEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip90Dvms/NIP90ContentDiscoveryResponseEvent.kt @@ -23,8 +23,8 @@ package com.vitorpamplona.quartz.nip90Dvms 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.core.HexKey import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip31Alts.AltTag 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 97784762e7..cfda406eab 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip90Dvms/NIP90StatusEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip90Dvms/NIP90StatusEvent.kt @@ -21,8 +21,8 @@ package com.vitorpamplona.quartz.nip90Dvms import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.nip89AppHandlers.recommendation.AppRecommendationEvent 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 9b12ccbd0b..d45c453409 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip90Dvms/NIP90UserDiscoveryRequestEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip90Dvms/NIP90UserDiscoveryRequestEvent.kt @@ -21,8 +21,8 @@ package com.vitorpamplona.quartz.nip90Dvms import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.nip89AppHandlers.recommendation.AppRecommendationEvent 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 6528f0ac31..96a87fb619 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip90Dvms/NIP90UserDiscoveryResponseEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip90Dvms/NIP90UserDiscoveryResponseEvent.kt @@ -21,8 +21,8 @@ package com.vitorpamplona.quartz.nip90Dvms import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.nip89AppHandlers.recommendation.AppRecommendationEvent 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 d61a777ab3..32f1b2cad9 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/FileHeaderEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/FileHeaderEvent.kt @@ -21,8 +21,8 @@ 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.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.core.any import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/IMetaTagBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/IMetaTagBuilderExt.kt index ff86151196..6cf3ca491a 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/IMetaTagBuilderExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/IMetaTagBuilderExt.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.quartz.nip94FileMetadata -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.nip36SensitiveContent.ContentWarningTag import com.vitorpamplona.quartz.nip92IMeta.IMetaTagBuilder diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/TagArrayBuilderExt.kt index cd4dfb4895..3a5c207fd2 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/TagArrayBuilderExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/TagArrayBuilderExt.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.quartz.nip94FileMetadata -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip94FileMetadata.tags.BlurhashTag import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag 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 index 38571c2300..69dfa8ded4 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/ImageTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/ImageTag.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.quartz.nip94FileMetadata.tags -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.utils.arrayOfNotNull class ImageTag( 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 index cd4d20cd05..e11faa1bef 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/ThumbTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/ThumbTag.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.quartz.nip94FileMetadata.tags -import com.vitorpamplona.quartz.nip01Core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.utils.arrayOfNotNull class ThumbTag( diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip96FileStorage/config/FileServersEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip96FileStorage/config/FileServersEvent.kt index 0952f016f7..b7350bd49f 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip96FileStorage/config/FileServersEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip96FileStorage/config/FileServersEvent.kt @@ -21,8 +21,8 @@ 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.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag 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 7b09bc39bd..08b5d27572 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip98HttpAuth/HTTPAuthorizationEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip98HttpAuth/HTTPAuthorizationEvent.kt @@ -21,8 +21,8 @@ package com.vitorpamplona.quartz.nip98HttpAuth import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip98HttpAuth.tags.MethodTag 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 index 437624e568..f8ae6d5830 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip98HttpAuth/tags/PayloadHashTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip98HttpAuth/tags/PayloadHashTag.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.quartz.nip98HttpAuth.tags -import com.vitorpamplona.quartz.nip01Core.HexKey -import com.vitorpamplona.quartz.nip01Core.toHexKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.utils.sha256.sha256 class PayloadHashTag { 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 7346ebd342..2e62394054 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip99Classifieds/ClassifiedsEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip99Classifieds/ClassifiedsEvent.kt @@ -21,8 +21,8 @@ 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.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.core.containsAllTagNamesWithValues import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/utils/sha256/Sha256Hasher.kt b/quartz/src/main/java/com/vitorpamplona/quartz/utils/sha256/Sha256Hasher.kt new file mode 100644 index 0000000000..533151a931 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/utils/sha256/Sha256Hasher.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.utils.sha256 + +import java.security.MessageDigest + +class Sha256Hasher { + val digest = MessageDigest.getInstance("SHA-256") + + fun hash(byteArray: ByteArray) = digest.digest(byteArray).also { digest.reset() } + + fun digest(byteArray: ByteArray) = digest.digest(byteArray) + + fun reset() = digest.reset() +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/utils/sha256/Sha256Pool.kt b/quartz/src/main/java/com/vitorpamplona/quartz/utils/sha256/Sha256Pool.kt index d94294be45..5b31af6fdc 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/utils/sha256/Sha256Pool.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/utils/sha256/Sha256Pool.kt @@ -21,40 +21,37 @@ package com.vitorpamplona.quartz.utils.sha256 import android.util.Log -import java.security.MessageDigest import java.util.concurrent.ArrayBlockingQueue class Sha256Pool( size: Int, ) { - private val pool = ArrayBlockingQueue(size) - - private fun digest() = MessageDigest.getInstance("SHA-256") + private val pool = ArrayBlockingQueue(size) init { repeat(size) { - pool.add(digest()) + pool.add(Sha256Hasher()) } } - private fun acquire(): MessageDigest { + private fun acquire(): Sha256Hasher { if (pool.size < 1) { Log.w("SHA256Pool", "Pool running low in available digests") } return pool.take() } - private fun release(digest: MessageDigest) { + private fun release(digest: Sha256Hasher) { digest.reset() pool.put(digest) } fun hash(byteArray: ByteArray): ByteArray { - val digest = acquire() + val hasher = acquire() try { - return digest.digest(byteArray) + return hasher.digest(byteArray) } finally { - release(digest) + release(hasher) } } } diff --git a/quartz/src/test/java/com/vitorpamplona/quartz/nip01Core/experimental/Nip01SerializerTest.kt b/quartz/src/test/java/com/vitorpamplona/quartz/nip01Core/experimental/Nip01SerializerTest.kt index 893413480d..f3f85c03ad 100644 --- a/quartz/src/test/java/com/vitorpamplona/quartz/nip01Core/experimental/Nip01SerializerTest.kt +++ b/quartz/src/test/java/com/vitorpamplona/quartz/nip01Core/experimental/Nip01SerializerTest.kt @@ -21,8 +21,8 @@ package com.vitorpamplona.quartz.nip01Core.experimental import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -import com.vitorpamplona.quartz.nip01Core.EventHasher import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.crypto.EventHasher import com.vitorpamplona.quartz.nip01Core.generateId import junit.framework.TestCase.assertEquals import org.junit.Test From d47ec06b1953adbf0a373afc1e85ab167df61414 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 18 Feb 2025 17:51:34 -0500 Subject: [PATCH 33/76] Deleting the experimental serializer test --- .../experimental/Nip01SerializerTest.kt | 212 ------------------ 1 file changed, 212 deletions(-) delete mode 100644 quartz/src/test/java/com/vitorpamplona/quartz/nip01Core/experimental/Nip01SerializerTest.kt diff --git a/quartz/src/test/java/com/vitorpamplona/quartz/nip01Core/experimental/Nip01SerializerTest.kt b/quartz/src/test/java/com/vitorpamplona/quartz/nip01Core/experimental/Nip01SerializerTest.kt deleted file mode 100644 index f3f85c03ad..0000000000 --- a/quartz/src/test/java/com/vitorpamplona/quartz/nip01Core/experimental/Nip01SerializerTest.kt +++ /dev/null @@ -1,212 +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.nip01Core.experimental - -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.crypto.EventHasher -import com.vitorpamplona.quartz.nip01Core.generateId -import junit.framework.TestCase.assertEquals -import org.junit.Test - -class Nip01SerializerTest { - val specialEncoders = - "Test\b\bTest\n\nTest\t\tTest\u000c\u000cTest\r\rTest\\Test\\\\Test\"Test/Test//Test" - - @Test() - fun fastJsonEncoderTest() { - val mapper = Nip01Serializer.StringWriter() - val expected = jacksonObjectMapper().writeValueAsString(specialEncoders) - - Nip01Serializer().escapeStringInto(specialEncoders, mapper) - val encoded = mapper.toString() - assertEquals(expected, "\"" + encoded + "\"") - } - - val payload2 = - """ -{ - "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/" - ] -} -""" - - @Test() - fun fastEventSerializerTest() { - val event = Event.fromJson(payload2) - val mapper = Nip01Serializer.StringWriter() - - Nip01Serializer().serializeEventInto(event, mapper) - - val encoded = mapper.toString() - val eventJson = EventHasher.makeJsonForId(event.pubKey, event.createdAt, event.kind, event.tags, event.content) - - assertEquals(eventJson, encoded) - } - - @Test() - fun fastEventIdCheckTest() { - val event = Event.fromJson(payload2) - - assertEquals("98b574c3527f0ffb30b7271084e3f07480733c7289f8de424d29eae82e36c758", event.generateId()) - } - - val payload3 = """ - { - "id": "6cccb576158965cf0f06fb4e476f85a02f0011ae783a4e905126a3db3871e43d", - "pubkey": "ee6ea13ab9fe5c4a68eaf9b1a34fe014a66b40117c50ee2a614f4cda959b6e74", - "created_at": 1698062466, - "kind": 14, - "tags": [ - [ - "p", - "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c" - ] - ], - "content": "Oh yeah I hadn't seen this ", - "sig": "" -}""" - - @Test() - fun fastEventSerializerTestPayload3() { - val event = Event.fromJson(payload3) - - val mapper = Nip01Serializer.StringWriter() - - Nip01Serializer().serializeEventInto(event, mapper) - - val encoded = mapper.toString() - val eventJson = EventHasher.makeJsonForId(event.pubKey, event.createdAt, event.kind, event.tags, event.content) - - assertEquals(eventJson, encoded) - } - - @Test() - fun fastEventIdCheckTestPayload3() { - val event = Event.fromJson(payload3) - - assertEquals("6cccb576158965cf0f06fb4e476f85a02f0011ae783a4e905126a3db3871e43d", event.generateId()) - } - - val payload4 = "{\"id\":\"5fd48fd3fb2890a00538067869306d788ff4331896360dc9c7e43d43e01b481b\",\"pubkey\":\"9770fb48aa3861dd393eb857e740f2df6f18e0ead43bad1d30c65e5c198200a6\",\"created_at\":1701673247,\"kind\":1,\"tags\":[[\"imeta\",\"url https://image.nostr.build/790b061d9661df88b06feb7448694cc421671da42217ca8381f02b4def63707f.jpg\",\"blurhash enF~?FRpNbkBjG.AkCbHfkafx_R+V^V_WVt9WBf6axoeoJf4bXWBaz\",\"dim 1536x2048\"],[\"imeta\",\"url https://video.nostr.build/3dd8562e8306c3128a72dee888c0fe587c2b24b8b643ae19b82010aaab95c37c.mp4\",\"blurhash e5A0~^S4R#W,j]~XR%s;o4j?s*M|t3t6ayxot9RiV[RjEH%3WBR%xb\",\"dim 720x1280\"],[\"imeta\",\"url https://image.nostr.build/9249f13cb8df68e00706f4f1434b8f0cd731b0515479df06353a0aef7a798619.jpg\",\"blurhash egFFpp\$LV?kCjs.TRiaej[fP9xIpoKf6fksls+WBbHj[xZn\$WWofjt\",\"dim 1920x3412\"],[\"t\",\"iceland\"],[\"r\",\"https://image.nostr.build/790b061d9661df88b06feb7448694cc421671da42217ca8381f02b4def63707f.jpg\"],[\"r\",\"https://video.nostr.build/3dd8562e8306c3128a72dee888c0fe587c2b24b8b643ae19b82010aaab95c37c.mp4\"],[\"r\",\"https://image.nostr.build/9249f13cb8df68e00706f4f1434b8f0cd731b0515479df06353a0aef7a798619.jpg\"]],\"content\":\"Icelandic calm to your heart \uD83D\uDC9A\uD83D\uDE0C\\n\\n(Memories from this summer)\\n\\n#Iceland , 2023 https://image.nostr.build/790b061d9661df88b06feb7448694cc421671da42217ca8381f02b4def63707f.jpg https://video.nostr.build/3dd8562e8306c3128a72dee888c0fe587c2b24b8b643ae19b82010aaab95c37c.mp4 https://image.nostr.build/9249f13cb8df68e00706f4f1434b8f0cd731b0515479df06353a0aef7a798619.jpg \",\"sig\":\"d6410be4b47bc97fca486eb619dd2507e7332bcd1049e405a047c90eedd2be46007c09d7702361b8442df78932e5da4055ee1c5fef08f938a4d39d828dc20957\"}" - - @Test() - fun fastEventSerializerTestPayload4() { - val event = Event.fromJson(payload4) - - val mapper = Nip01Serializer.StringWriter() - - Nip01Serializer().serializeEventInto(event, mapper) - - val encoded = mapper.toString() - val eventJson = EventHasher.makeJsonForId(event.pubKey, event.createdAt, event.kind, event.tags, event.content) - - assertEquals(eventJson, encoded) - } - - @Test() - fun fastEventIdCheckTestPayload4() { - val event = Event.fromJson(payload4) - - // assertEquals(event.generateId(), event.generateId2()) - assertEquals("5fd48fd3fb2890a00538067869306d788ff4331896360dc9c7e43d43e01b481b", event.generateId()) - } - - val payload5 = "{\"id\":\"d1f097d3d9fcfb00df0c8ab5469be6484b14707d1e947c574ed636281d8dfd26\",\"pubkey\":\"dd664d5e4016433a8cd69f005ae1480804351789b59de5af06276de65633d319\",\"created_at\":1706435280,\"kind\":4550,\"tags\":[[\"a\",\"34550:026d8b7e7bcc2b417a84f10edb71b427fe76069905090b147b401a6cf60c3f27:Catholic\",\"wss://christpill.nostr1.com\"],[\"e\",\"0b8e4fade30fdb57f3887da224682fe9756ee79c408961e46393555bb0367022\"],[\"p\",\"026d8b7e7bcc2b417a84f10edb71b427fe76069905090b147b401a6cf60c3f27\"],[\"k\",\"1\"]],\"content\":\"{\\\"id\\\":\\\"0b8e4fade30fdb57f3887da224682fe9756ee79c408961e46393555bb0367022\\\",\\\"pubkey\\\":\\\"026d8b7e7bcc2b417a84f10edb71b427fe76069905090b147b401a6cf60c3f27\\\",\\\"created_at\\\":1698838786,\\\"kind\\\":1,\\\"tags\\\":[[\\\"a\\\",\\\"34550:026d8b7e7bcc2b417a84f10edb71b427fe76069905090b147b401a6cf60c3f27:Catholic\\\",\\\"\\\",\\\"reply\\\"],[\\\"t\\\",\\\"catholic\\\"],[\\\"t\\\",\\\"catholic\\\"]],\\\"content\\\":\\\"It's so funny at Mass, when you're a sacristan or something because everyone watches and emulates you, so if you forget to stand or kneel at the right moment, everyone remains seated.\\\\n\\\\nAnd when you're like, Oh woops! And stand up, there's a loud wave of people suddenly standing up, too. \uD83D\uDE02\\\\n\\\\n#catholic\\\",\\\"sig\\\":\\\"92c087e6364dd6c1fbf3bf5baddd66f0d86019fb3ba95e36135345d7c8b137f2147de8d628ec974eb493c2dbce2ca581c56c146d282f06a930280c7addc4d021\\\"}\",\"sig\":\"d67c067a08879138989275cb6f062f58e8a523b192bff5e65340becbac05060bb9c7ac6f75727c8fd2229f95a54fe404d18971950a72c1f17618535e7495e09d\"}" - - @Test() - fun fastEventSerializerTestPayload5() { - val event = Event.fromJson(payload5) - - val mapper = Nip01Serializer.StringWriter() - - Nip01Serializer().serializeEventInto(event, mapper) - - val encoded = mapper.toString() - val eventJson = EventHasher.makeJsonForId(event.pubKey, event.createdAt, event.kind, event.tags, event.content) - - assertEquals(eventJson, encoded) - } - - @Test() - fun fastEventIdCheckTestPayload5() { - val event = Event.fromJson(payload5) - - assertEquals("d1f097d3d9fcfb00df0c8ab5469be6484b14707d1e947c574ed636281d8dfd26", event.generateId()) - } -} From 06240b1f1543b42c2fa7821830a7975962b1694c Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 18 Feb 2025 18:14:52 -0500 Subject: [PATCH 34/76] Updates libsecp256k --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8d874cd70e..ef2c2ae596 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -39,7 +39,7 @@ navigationCompose = "2.8.7" okhttp = "5.0.0-alpha.14" runner = "1.6.2" rfc3986 = "0.1.2" -secp256k1KmpJniAndroid = "0.16.0" +secp256k1KmpJniAndroid = "0.17.1" securityCryptoKtx = "1.1.0-alpha06" spotless = "6.25.0" torAndroid = "0.4.8.12" From af291332724b0a1aeb380022e7774525bf76a1b7 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 18 Feb 2025 19:00:05 -0500 Subject: [PATCH 35/76] Simplifies the benchmark for PoW --- .../vitorpamplona/quartz/benchmark/PoWBenchmark.kt | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/PoWBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/PoWBenchmark.kt index 94011874b9..fd3b2bc954 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/PoWBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/PoWBenchmark.kt @@ -24,14 +24,12 @@ import androidx.benchmark.junit4.BenchmarkRule import androidx.benchmark.junit4.measureRepeated import androidx.test.ext.junit.runners.AndroidJUnit4 import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray +import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate -import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip13Pow.miner.PoWMiner import com.vitorpamplona.quartz.nip13Pow.miner.PoWRankEvaluator -import com.vitorpamplona.quartz.nip13Pow.pow -import com.vitorpamplona.quartz.nip13Pow.powNonce import org.junit.Assert.assertEquals import org.junit.Rule import org.junit.Test @@ -70,14 +68,9 @@ class PoWBenchmark { @Test fun generatePow() { - val signer = NostrSignerSync(KeyPair()) - var template: EventTemplate? = null benchmarkRule.measureRepeated { - template = PoWMiner.run(baseTemplate, signer.pubKey, 5) + PoWMiner.run(baseTemplate, KeyPair().pubKey.toHexKey(), 5) } - val event = signer.sign(template!!) - assertEquals(5, event?.pow()) - println("POW: " + event?.powNonce()?.nonce) } @Test From 94e0f4ed025cbb6df1c4b9bc80afaddb82f69afc Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 18 Feb 2025 20:44:24 -0500 Subject: [PATCH 36/76] Performance improvements on the PoW miner --- .../quartz/nip13Pow/miner/ByteArrayExt.kt | 10 ---------- .../vitorpamplona/quartz/nip13Pow/miner/PoWMiner.kt | 13 ++++++++----- .../quartz/nip13Pow/miner/PoWRankEvaluator.kt | 13 +++++++++++++ 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/miner/ByteArrayExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/miner/ByteArrayExt.kt index b4dfbc714f..f931ecdebf 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/miner/ByteArrayExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/miner/ByteArrayExt.kt @@ -37,13 +37,3 @@ fun ByteArray.indexOf(sequence: ByteArray): Int { } return -1 } - -fun ByteArray.set( - value: ByteArray, - startIndex: Int, -) { - var index = startIndex - for (byte in value) { - this[index++] = byte - } -} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/miner/PoWMiner.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/miner/PoWMiner.kt index 3169e49e6e..bd3984e9e3 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/miner/PoWMiner.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/miner/PoWMiner.kt @@ -32,8 +32,9 @@ class PoWMiner( val desiredPoW: Int, ) { val hasher = Sha256Hasher() + val emptyBytesForDesiredPoW = desiredPoW / 8 - fun rank(byteArray: ByteArray) = PoWRankEvaluator.calculatePowRankOf(hasher.hash(byteArray)) + fun reachedDesiredPoW(byteArray: ByteArray) = PoWRankEvaluator.atLeastPowRank(hasher.hash(byteArray), desiredPoW, emptyBytesForDesiredPoW) fun run() = runDigit(buffer.nonceStarts) @@ -42,15 +43,17 @@ class PoWMiner( // replaces the background base by the nonce integers buffer.bytes[index] = testByte - if (rank(buffer.bytes) >= desiredPoW) return true - - if (index + 1 < buffer.nonceEnds && runDigit(index + 1)) return true + if (index + 1 < buffer.nonceEnds) { + if (runDigit(index + 1)) return true + } else { + if (reachedDesiredPoW(buffer.bytes)) return true + } } return false } companion object { - private val STARTING_NONCE_SIZE = 5 + private const val STARTING_NONCE_SIZE = 5 // make sure these chars are not escaped by the JSON stringifier private val VALID_CHARS: List = diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/miner/PoWRankEvaluator.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/miner/PoWRankEvaluator.kt index 0a65d591f7..5b521b180c 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/miner/PoWRankEvaluator.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip13Pow/miner/PoWRankEvaluator.kt @@ -103,5 +103,18 @@ class PoWRankEvaluator { } return rank } + + @JvmStatic + fun atLeastPowRank( + id: ByteArray, + minPoW: Int, + emptyBytes: Int, + ): Boolean { + for (index in 0 until emptyBytes) { + if (id[index] != R8) return false + } + + return calculatePowRankOf(id) >= minPoW + } } } From aaf86bf53e1386f8164ce6e27376f07cd94d4308 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 20 Feb 2025 14:05:58 -0500 Subject: [PATCH 37/76] Adds a Bloom-based hint indexer with MurMur hash --- .../vitorpamplona/amethyst/model/Account.kt | 2 +- .../com/vitorpamplona/amethyst/model/Note.kt | 2 +- .../ui/actions/NewChannelViewModel.kt | 2 +- .../amethyst/ui/actions/NewPostViewModel.kt | 2 +- .../loggedIn/chatrooms/ChannelScreen.kt | 2 +- .../benchmark/BloomFilterMurMur3Benchmark.kt | 135 + .../quartz/benchmark/HintIndexerBenchmark.kt | 88 + quartz/src/androidTest/assets/relayDB.txt | 2539 +++++++++++++++++ .../vitorpamplona/quartz/bloom/BloomFilter.kt | 11 +- .../bounties/BountyAddValueEvent.kt | 2 +- .../nip95/header/FileStorageHeaderEvent.kt | 2 +- .../nip01Core/{ => hints}/EventHintBundle.kt | 2 +- .../quartz/nip01Core/hints/HintIndexer.kt | 49 + .../quartz/nip01Core/hints/HintProvider.kt | 33 + .../quartz/nip01Core/hints/bloom/BitSetExt.kt | 30 + .../hints/bloom/BloomFilterMurMur3.kt | 83 + .../nip01Core/hints/bloom/MurmurHash3.kt | 121 + .../nip01Core/hints/types/AddressHint.kt | 28 + .../nip01Core/hints/types/EventIdHint.kt | 31 + .../quartz/nip01Core/hints/types/Hint.kt | 25 + .../nip01Core/hints/types/PubKeyHint.kt | 31 + .../quartz/nip03Timestamp/OtsEvent.kt | 2 +- .../nip04Dm/messages/TagArrayBuilderExt.kt | 2 +- .../quartz/nip10Notes/TextNoteEvent.kt | 2 +- .../quartz/nip10Notes/tags/ReplyBuilder.kt | 2 +- .../quartz/nip17Dm/NIP17Factory.kt | 2 +- .../ChatMessageEncryptedFileHeaderEvent.kt | 2 +- .../nip17Dm/files/TagArrayBuilderExt.kt | 2 +- .../nip17Dm/messages/ChatMessageEvent.kt | 2 +- .../nip17Dm/messages/TagArrayBuilderExt.kt | 2 +- .../quartz/nip21UriScheme/EventExt.kt | 2 +- .../quartz/nip22Comments/CommentEvent.kt | 2 +- .../quartz/nip25Reactions/ReactionEvent.kt | 2 +- .../admin/ChannelHideMessageEvent.kt | 2 +- .../admin/ChannelMetadataEvent.kt | 2 +- .../admin/ChannelMuteUserEvent.kt | 2 +- .../base/TagArrayBuilderExt.kt | 2 +- .../message/ChannelMessageEvent.kt | 2 +- .../selection/EmojiPackSelectionEvent.kt | 2 +- .../quartz/nip34Git/issue/GitIssueEvent.kt | 2 +- .../nip34Git/issue/TagArrayBuilderExt.kt | 2 +- .../quartz/nip34Git/reply/GitReplyEvent.kt | 2 +- .../nip34Git/reply/TagArrayBuilderExt.kt | 2 +- .../nip35Torrents/TorrentCommentEvent.kt | 2 +- .../chat/LiveActivitiesChatMessageEvent.kt | 2 +- .../chat/TagArrayBuilderExt.kt | 2 +- .../approval/CommunityPostApprovalEvent.kt | 2 +- .../approval/TagArrayBuilderExt.kt | 2 +- .../quartz/nip75ZapGoals/GoalEvent.kt | 2 +- .../quartz/utils/RandomInstance.kt | 2 +- .../nip01Core/hints/BloomFilterMurMur3Test.kt | 114 + .../quartz/nip01Core/hints/HintIndexerTest.kt | 93 + .../quartz/nip01Core/hints/relayDB.txt | 2539 +++++++++++++++++ 53 files changed, 5981 insertions(+), 43 deletions(-) create mode 100644 benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/BloomFilterMurMur3Benchmark.kt create mode 100644 benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/HintIndexerBenchmark.kt create mode 100644 quartz/src/androidTest/assets/relayDB.txt rename quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/{ => hints}/EventHintBundle.kt (98%) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/HintIndexer.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/HintProvider.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/bloom/BitSetExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/bloom/BloomFilterMurMur3.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/bloom/MurmurHash3.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/types/AddressHint.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/types/EventIdHint.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/types/Hint.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/types/PubKeyHint.kt create mode 100644 quartz/src/test/java/com/vitorpamplona/quartz/nip01Core/hints/BloomFilterMurMur3Test.kt create mode 100644 quartz/src/test/java/com/vitorpamplona/quartz/nip01Core/hints/HintIndexerTest.kt create mode 100644 quartz/src/test/java/com/vitorpamplona/quartz/nip01Core/hints/relayDB.txt 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 bd52a61d59..b1593b09d9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -75,11 +75,11 @@ 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.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair +import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate 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 2453a4bbfb..9868922cad 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -40,10 +40,10 @@ 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.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address 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 e8278ce56e..de37917fdd 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,7 +26,7 @@ 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.hints.EventHintBundle import com.vitorpamplona.quartz.nip01Core.tags.events.ETag import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent 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 2a69173b36..7d9a6f0fdc 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 @@ -66,10 +66,10 @@ 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.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip01Core.tags.events.ETag import com.vitorpamplona.quartz.nip01Core.tags.events.eTags import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohash 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 38e95cb178..fa00e70a29 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,7 +165,7 @@ 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.nip01Core.EventHintBundle +import com.vitorpamplona.quartz.nip01Core.hints.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 diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/BloomFilterMurMur3Benchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/BloomFilterMurMur3Benchmark.kt new file mode 100644 index 0000000000..c4579f013e --- /dev/null +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/BloomFilterMurMur3Benchmark.kt @@ -0,0 +1,135 @@ +/** + * 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.benchmark + +import androidx.benchmark.junit4.BenchmarkRule +import androidx.benchmark.junit4.measureRepeated +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray +import com.vitorpamplona.quartz.nip01Core.hints.bloom.BloomFilterMurMur3 +import com.vitorpamplona.quartz.utils.RandomInstance +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class BloomFilterMurMur3Benchmark { + @get:Rule val benchmarkRule = BenchmarkRule() + + val testEncoded = "100:10:AKiEIEQKALgRACEABA==:3" + + val key1 = "ca29c211f1c72d5b6622268ff43d2288ea2b2cb5b9aa196ff9f1704fc914b71b".hexToByteArray() + val key2 = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c".hexToByteArray() + val key3 = "560c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c".hexToByteArray() + + val keys = + mutableListOf().apply { + for (seed in 0..1000000) { + add(RandomInstance.bytes(32)) + } + } + + val keys2 = + mutableListOf().apply { + for (seed in 0..1000000) { + add(RandomInstance.bytes(32)) + } + } + + @Test + fun addExisting() { + val filter = BloomFilterMurMur3.decode(testEncoded) + benchmarkRule.measureRepeated { + filter.add(key1) + } + } + + @Test + fun addNew() { + val filter = BloomFilterMurMur3.decode(testEncoded) + benchmarkRule.measureRepeated { + filter.add(key3) + } + } + + @Test + fun mightContainTrue() { + val filter = BloomFilterMurMur3(10_000_000, 5) + filter.add(key1) + keys.forEach(filter::add) + benchmarkRule.measureRepeated { + filter.mightContain(key1) + } + } + + @Test + fun mightContainFalse() { + val filter = BloomFilterMurMur3(10_000_000, 5) + keys.forEach(filter::add) + benchmarkRule.measureRepeated { + filter.mightContain(key3) + } + } + + @Test + fun decode() { + benchmarkRule.measureRepeated { + BloomFilterMurMur3.decode(testEncoded) + } + } + + @Test + fun encode() { + val filter = BloomFilterMurMur3.decode(testEncoded) + benchmarkRule.measureRepeated { + filter.encode() + } + } + + @Test + fun largeFilterBuild() { + val bloomFilter = BloomFilterMurMur3(10_000_000, 5) + + benchmarkRule.measureRepeated { + keys.forEach(bloomFilter::add) + } + } + + @Test + fun largeFilterCheckExisting() { + val bloomFilter = BloomFilterMurMur3(10_000_000, 5) + keys.forEach(bloomFilter::add) + + benchmarkRule.measureRepeated { + keys.forEach(bloomFilter::mightContain) + } + } + + @Test + fun largeFilterCheckNew() { + val bloomFilter = BloomFilterMurMur3(10_000_000, 5) + keys.forEach(bloomFilter::add) + + benchmarkRule.measureRepeated { + keys2.forEach(bloomFilter::mightContain) + } + } +} diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/HintIndexerBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/HintIndexerBenchmark.kt new file mode 100644 index 0000000000..0baf72bc8f --- /dev/null +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/HintIndexerBenchmark.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.benchmark + +import androidx.benchmark.junit4.BenchmarkRule +import androidx.benchmark.junit4.measureRepeated +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import com.vitorpamplona.quartz.nip01Core.hints.HintIndexer +import com.vitorpamplona.quartz.utils.RandomInstance +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import java.nio.charset.Charset + +@RunWith(AndroidJUnit4::class) +class HintIndexerBenchmark { + @get:Rule val benchmarkRule = BenchmarkRule() + + val keys = + mutableListOf().apply { + for (seed in 0..1_000_000) { + add(RandomInstance.bytes(32)) + } + } + + val relays = + getInstrumentation() + .context.assets + .open("relayDB.txt") + .readBytes() + .toString(Charset.forName("utf-8")) + .split("\n") + + @Test + fun relayUriHashcode() { + benchmarkRule.measureRepeated { + "wss://relay.bitcoin.social".hashCode() + } + } + + @Test + fun getRelayHints() { + val indexer = HintIndexer() + + keys.forEach { key -> + (0..5).map { + indexer.index(key, relays.random()) + } + } + + val key = keys.random() + + benchmarkRule.measureRepeated { + indexer.get(key) + } + } + + @Test + fun buildIndexer() { + benchmarkRule.measureRepeated { + val indexer = HintIndexer() + keys.forEach { key -> + (0..5).map { + indexer.index(key, relays.random()) + } + } + } + } +} diff --git a/quartz/src/androidTest/assets/relayDB.txt b/quartz/src/androidTest/assets/relayDB.txt new file mode 100644 index 0000000000..0977f44b3a --- /dev/null +++ b/quartz/src/androidTest/assets/relayDB.txt @@ -0,0 +1,2539 @@ +wss://nostr.wine +wss://relay.orangepill.dev +wss://xmr.usenostr.org +wss://nostr.portemonero.com +wss://nostr.xmr.rocks +wss://relay.nostr.band +wss://filter.nostr.wine +wss://nostr.milou.lol +wss://nostr.mutinywallet.com +wss://nostr-pub.wellorder.net +wss://nostr.zebedee.cloud +wss://nos.lol +wss://brb.io +wss://bitcoiner.social +wss://nostr.decentony.com +wss://relay.nostriches.org +wss://paid.spore.ws +wss://eden.nostr.land +wss://puravida.nostr.land +wss://5dzvuefllevkhk7miqynaviguedxfnofrayu2xwfwtlkdg4radjdlyqd.onion +wss://relay-jp.nostr.wirednet.jp +wss://relay.nostrich.land +wss://nostr.holybea.com +wss://nostr-relay.nokotaro.com +wss://nostr-paid.h3z.jp +wss://nostrja-kari.heguro.com +wss://nostr.mom +wss://nostr.fediverse.jp +wss://nostr.h3z.jp +wss://universe.nostrich.land +wss://nostr.uselessshit.co +wss://atlas.nostr.land +wss://relay.snort.social +wss://universe.nostrich.landlangenlanges +wss://nostr.slothy.win +wss://nostr.plebchain.org +wss://nostr-relay.untethr.me +wss://relay.nostr.com.au +wss://nostr.inosta.cc +wss://relay.nostrati.com +wss://nostr.bitcoiner.social +wss://relay.nostrplebs.com +wss://relay.nostr.info +wss://nostr-relay.wlvs.space +wss://nostr.oxtr.dev +wss://nostr.onsats.org +wss://relay.wellorder.net +wss://relay.plebstr.com +wss://no.str.cr +wss://nostr.walletofsatoshi.com +wss://nostr.mwmdev.com +wss://relay.nostr.bg +wss://nostr.rocks +wss://nostr.fmt.wiz.biz +wss://nostr.orangepill.dev +wss://nostr-pub.semisol.dev +wss://nostr.sandwich.farm +wss://relay.nostr.ch +wss://relay.orange-crush.com +wss://private.red.gb.net +wss://nostr.lnprivate.network +wss://nostr.lu.ke +wss://relay.nostr.wirednet.jp +wss://lightningrelay.com +wss://relay.nostrgraph.net +wss://relay.nostrica.com +wss://relay.mostr.pub +wss://nostr-sg.com +wss://nostr.zkid.social +wss://relay.nostr.vet +wss://relay.nostr3.io +wss://relay.arsip.my.id +wss://relay.current.fyi +wss://global.relay.red +wss://nostr.island.network +wss://node01.nostress.cc +wss://relay.nostr.net.in +wss://relay.utxo.one +wss://relay-1.arsip.my.id +wss://nostrical.com +wss://nostro.cc +wss://rsslay.nostr.moe +wss://relay.nostr.or.jp +wss://nostream.unift.xyz +wss://blastr.f7z.xyz +wss://relay.honk.pw +wss://universe.nostrich.landlangja +wss://nostream.ocha.one +wss://paid-relay.nost.love +wss://offchain.pub +wss://nostr-usa.ka1gbeoa21bnm.us-west-2.cs.amazonlightsail.com +wss://nostr.terminus.money +wss://nostr.shawnyeager.net +wss://public.nostr.swissrouting.com +wss://nostrex.fly.dev +wss://relay.727whisky.com +wss://relay.cryptocculture.com +wss://relay.bleskop.com +wss://nostr.lorentz.is +wss://nostr.actn.io +wss://nostr-relay.lnmarkets.com +wss://nostr.openchain.fr +wss://relay.punkhub.me +wss://nostr.blipme.app +wss://nostr.swiss-enigma.ch +wss://nostr-verified.wellorder.net +wss://at.nostrworks.com +wss://21sats.net +wss://e.nos.lol +wss://nostr.mikedilger.com +wss://nostr.azte.co +wss://noster.bitcoiner.social +wss://relay.nostr.moe +wss://nostream.nostrly.io +wss://nostr.gives.africa +wss://nostr.21sats.net +wss://bitcoinmaximalists.online +wss://paid.nostrified.org +wss://nostr.1sat.org +wss://nostr.relayer.se +wss://sg.qemura.xyz +wss://nostr.coinos.io +wss://nostr.bitcoinplebs.de +wss://nostrich.friendship.tw +wss://nostr.sg +wss://eosla.com +wss://nostr.sidnlabs.nl +wss://nostr.foundrydigital.com +wss://relay.nostrview.com +wss://nostr.semisol.dev +wss://relay.f7z.io +wss://wlvs.space +wss://nostr.v0l.io +wss://nostr-relay.digitalmob.ro +wss://rsslay.fiatjaf.com +wss://relay.theorangepillapp.com +wss://relay.zeh.app +wss://nostr.zoomout.chat +wss://relay.stoner.com +wss://nostr.cercatrova.me +wss://relay.ryzizub.com +wss://nostr-1.nbo.angani.co +wss://nostr21.com +wss://spore.ws +wss://nostrue.com +wss://no-str.org +wss://relay.taxi +wss://ragnar-relay.com +wss://relay.austrich.net +wss://relay.nostr-latam.link +wss://1.noztr.com +wss://relay.nostr.scot +wss://test.relay.nostrich.day +wss://jiggytom.ddns.net +wss://nostr.bongbong.com +wss://relay.nostromo.social +wss://relay.sendstr.com +wss://nostr-dev.universalname.space +wss://relay.nostrcheck.me +wss://nostr.libertasprimordium.com +wss://nostr.kollider.xyz +wss://expensive-relay.fiatjaf.com +wss://nostr-sandbox.minds.io +wss://relay.nostrich.de +wss://nostr.gromeul.eu +wss://relay.nostr.wine +wss://nostr.screaminglife.io +wss://nostr-relay.derekross.me +wss://nostrica.nostrnotes.com +wss://paid.no.str.cr +wss://nostr.sethforprivacy.com +wss://nostr.dumpit.top +wss://nostr.herci.one +wss://cheery-paddock-rsakdrtc35c55n6yregn.wnext.app +wss://nostr.blockpower.capital +wss://nostr.nym.life +wss://nostr-verif.slothy.win +wss://fiatdenier.com +wss://nostr.bitcoin-21.org +wss://nostr.fluidtrack.in +wss://nostr.developer.li +wss://r.ayit.org +wss://relay.nostr.nu +wss://nostr.bostonbtc.com +wss://rly.social +wss://nostr.bridgey.dev +wss://relay.nostrprotocol.net +wss://nostr.mado.io +wss://nostr.einundzwanzig.space +wss://nostr2.actn.io +wss://nostr-relay.freedomnode.com +wss://nostr.pleb.network +wss://nostr.mouton.dev +wss://eelay.current.fyi +wss://nostr.notmyhostna.me +wss://nostr.pjv.me +wss://nostr.jatm.link +wss://nostr.fractalized.ovh +wss://nostr-relay.app.ikeji.ma +wss://relayer.ocha.one +wss://nostr.com.de +wss://nostr-2.afarazit.eu +wss://nostr.l00p.org +wss://nostr.drss.io +wss://relay.nostrify.io +wss://nostr.radixrat.com +wss://nostr-relay.bitcoin.ninja +wss://nostrsatva.net +wss://nostr.mustardnodes.com +wss://nostr01.vida.dev +wss://nostr.noones.com +wss://nostr.easify.de +wss://nostr3.actn.io +wss://moonbreeze.richardbondi.net +wss://nostr.naut.social +wss://private-nostr.v0l.io +wss://nostr.zaprite.io +wss://nostr.lightninglinks.xyz +wss://nostr.hackerman.pro +wss://nr.yay.so +wss://nostr.roundrockbitcoiners.com +wss://nostr.sovbit.host +wss://nostrelay.yeghro.site +wss://pow32.nostr.land +wss://nostr.1729.cloud +wss://nostr.rdfriedl.com +wss://nostr.h4x0r.host +wss://nostr.up.railway.app +wss://nostr.lnorb.com +wss://nostr.lordkno.ws +wss://relay.nostr.vision +wss://nostr-3.orba.ca +wss://satstacker.cloud +wss://freedom-relay.herokuapp.com +wss://nostr-relay.freeberty.net +wss://nostr.unknown.place +wss://nostr.delo.software +wss://relay.nostr.pro +wss://relay.minds.com +wss://nostr.ono.re +wss://relay.grunch.dev +wss://relay.cynsar.foundation +wss://relay.oldcity-bitcoiners.info +wss://relay.bitid.nz +wss://relay.nostr.xyz +wss://relay.futohq.com +wss://relay.farscapian.com +wss://astral.ninja +wss://relay.sovereign-stack.org +wss://nostr-2.zebedee.cloud +wss://nostr.nymsrelay.com +wss://relay.kronkltd.net +wss://relay.r3d.red +wss://universe.nostrich.landlangen +wss://nostr-dev.wellorder.net +wss://nostr.beta3.dev +wss://nostr.data.haus +wss://nostr.hugo.md +wss://relay-dev.cowdle.gg +wss://relay.dwadziesciajeden.pl +wss://tmp-relay.cesc.trade +wss://nostr.massmux.com +wss://relay.nostr.africa +wss://nostr1.tunnelsats.com +wss://nostr.f44.dev +wss://relay.n057r.club +wss://nostr-verif.slothy.com +wss://nostr.1f52b.xyz +wss://nostr.sebastix.dev +wss://nostr.lightning.contact +wss://nostr.rly.social +wss://noster.online +wss://relay.lexingtonbitcoin.org +wss://nostr.bitcoinbay.engineering +wss://nostr.howtobitcoin.shop +wss://blg.nostr.sx +wss://deschooling.us +wss://foolay.nostr.moe +wss://freespeech.casa +wss://nostr-01.bolt.observer +wss://nostr-01.dorafactory.org +wss://nostr-au.coinfundit.com +wss://nostr-eu.coinfundit.com +wss://nostr-relay.alekberg.net +wss://nostr-pub1.southflorida.ninja +wss://nostr-relay.gkbrk.com +wss://nostr-relay.pcdkd.fyi +wss://nostr-relay.schnitzel.world +wss://nostr-us.coinfundit.com +wss://nostr.21crypto.ch +wss://nostr.600.wtf +wss://nostr.8e23.net +wss://nostr.app.runonflux.io +wss://nostr.arguflow.gg +wss://nostr.bch.ninja +wss://nostr.chainofimmortals.net +wss://nostr.cizmar.net +wss://nostr.cheeserobot.org +wss://nostr.coollamer.com +wss://nostr.corebreach.com +wss://nostr.cro.social +wss://nostr.easydns.ca +wss://nostr.globals.fans +wss://nostr.handyjunky.com +wss://nostr.itas.li +wss://nostr.sectiontwo.org +wss://nostr.spleenrider.one +wss://nostr.thibautrey.fr +wss://nostr.uthark.com +wss://nostr.vulpem.com +wss://nostr.w3ird.tech +wss://nostr.whoop.ph +wss://nostr.yuv.al +wss://nostr01.opencult.com +wss://nostre.cc +wss://nostream.denizenid.com +wss://nostring.deno.dev +wss://pow.nostrati.com +wss://relay-pub.deschooling.us +wss://nostr.jiashanlu.synology.me +wss://nostr.klabo.blog +wss://relay.valireum.net +wss://nostr.fly.dev +wss://nostr.nordlysln.net +wss://nostr.zerofeerouting.com +wss://rsslay.nostr.net +wss://nostr-relay.nonce.academy +wss://nostr.rewardsbunny.com +wss://lv01.tater.ninja +wss://nostr-2.orba.ca +wss://nostr.orba.ca +wss://nostr.supremestack.xyz +wss://nostrrelay.com +wss://relay.nostr.au +wss://nostr.oooxxx.ml +wss://nostr.yael.at +wss://nostr-relay.trustbtc.org +wss://nostr.namek.link +wss://nostr-relay.wolfandcrow.tech +wss://nostr.satsophone.tk +wss://relay.dev.kronkltd.net +wss://nostr2.namek.link +wss://relay.21spirits.io +wss://relay.minds.io +wss://nostr.d11n.net +wss://nostr.tunnelsats.com +wss://nostr.leximaster.com +wss://mule.platanito.org +wss://nostr.robotechy.com +wss://relay.nostrmoto.xyz +wss://relay.boring.surf +wss://nostr.gruntwerk.org +wss://nostr.hyperlingo.com +wss://nostr.ethtozero.fr +wss://nostr.nodeofsven.com +wss://nostr.jimc.me +wss://nostr.utxo.lol +wss://relay.nyx.ma +wss://nostr.shmueli.org +wss://wizards.wormrobot.org +wss://nostr.sovbit.com +wss://nostr.datamagik.com +wss://relay.nostrid.com +wss://nostr1.starbackr.me +wss://relay.nostr.express +wss://nostr.formigator.eu +wss://nostr.xpersona.net +wss://nostr.digitalreformation.info +wss://nostr-relay.usebitcoin.space +wss://nostr-alpha.gruntwerk.org +wss://nostr-relay.australiaeast.cloudapp.azure.com +wss://nostr-relay.smoove.net +wss://nostr-relay.j3s7m4n.com +wss://nostr.demovement.net +wss://nostr.thesimplekid.com +wss://nostr.aozing.com +wss://nostr.blocs.fr +wss://no.str.watch +wss://btc.klendazu.com +wss://nostr.mrbits.it +wss://nostr.zenon.wtf +wss://no.contry.xyz +wss://nostream.gromeul.eu +wss://relay.nostr.ro +wss://nostr.ncsa.illinois.edu +wss://nostr.itssilvestre.com +wss://nostr.chaker.net +wss://knostr.neutrine.com +wss://nostr.pobblelabs.org +wss://nostr.simatime.com +wss://relay.nosphr.com +wss://student.chadpolytechnic.com +wss://nostr.localhost.re +wss://nostr.coinsamba.com.br +wss://deconomy-netser.ddns.net:2121 +wss://nostr.21m.fr +wss://zur.nostr.sx +wss://nostr-relay.texashedge.xyz +wss://spleenrider.herokuapp.com +wss://nostr.bitcoin.sex +wss://relay.nostrzoo.com +wss://nostr.blockchaincaffe.it +wss://nostr-bg01.ciph.rs +wss://knostr.neutrine.com:8880 +wss://nostr.ahaspharos.de +wss://nostr.argdx.net +wss://nostr.snblago.com +wss://merrcurr.up.railway.app +wss://nostr.bingtech.tk +wss://relay.nostr.wf +wss://relay.koreus.social +wss://nostr.randomdevelopment.biz +wss://relay.nostr.hu +wss://relay.nostr.lu +wss://relay.nostr.ae +wss://middling.myddns.me:8080 +wss://nostr.nikolaj.online +wss://relay.nostrology.org +wss://nostr.satoshi.fun +wss://nostream.kinchie.snowinning.com +wss://nostr.lapalomilla.mx +wss://relay.thes.ai +wss://rsr.uyky.net:30443 +wss://nostrafrica.pcdkd.fyi +wss://nostr.bitcoin-basel.ch +wss://relay.21baiwan.com +wss://nostr.ddns.net:8008 +wss://free-relay.nostrich.land +wss://nostr.lukeacl.com +wss://nostr.ddns.net +wss://nostr.rocket-tech.net +wss://nostr-1.afarazit.eu +wss://nostr.0nyx.eu +wss://nostr-mv.ashiroid.com +wss://lbrygen.xyz +wss://nostr.community.networks.deavmi.assigned.network +wss://nostr.ownscale.org +wss://relay1.gems.xyz +wss://nostr.soscary.net +wss://nostr.0xtr.dev +wss://damus.io +wss://relay.alien.blue +wss://nostr.btcmp.com +wss://relayer.fiatjaf.com +wss://relay.lacosanostr.com +wss://adult.18plus.social +wss://nostrrr.bublina.eu.org +wss://relay.stoner +wss://nostr.pwnshop.cloud +wss://nostr.directory +wss://nostr-relay-dev.wlvs.space +wss://member.cash +wss://relay.nyc1.vinux.app +wss://nostr-relay.digitamob.ro +wss://nor.st +wss://nostr.topeth.info +wss://nostr.rocketstyle.com.au +wss://relay.tnano.duckdns.org +wss://nostr.21l.st +wss://electra.nostr.land +wss://relay.codl.co +wss://nostr.koning-degraaf.nl +wss://relay.mrjohnsson.net +wss://nostr.thank.eu +wss://relay.stonez.me +wss://relay.nostr.distrl.net +wss://relay.valera.co +wss://api.semisol.dev +wss://nostr.lol +wss://relay.shitforce.one +wss://n-word.sharivegas.com +wss://lamp.wtf +wss://nostr.bitcoinpuertori.co +wss://nostr-01.bolt.oberver +wss://3d515c5277e9.ngrok.io +wss://nostr.xmrk.mooo.com +wss://alphapanda.pro +wss://relays.world +wss://universe.nostrich.landlangzh +wss://arnostr.permadao.io +wss://relay.chenxixian.cn +wss://universe.nostrich.landlangzhlangen +wss://v2r.chenxixian.cn +wss://nostr-relay-test.nokotaro.work +wss://universe.nostrich.landlangjalangen +wss://nostr.risa.zone +wss://relay.nosbin.com +wss://translate.argosopentech.com +wss://edennostr.land +wss://nostr.kawagarbo.xyz +wss://nostr.member.cash +wss://ch1.duno.com +wss://nostream-production-b80e.up.railway.app +wss://relay1.nostrich.cloud +wss://relay.t5y.ca +wss://nostr.zhongwen.world +wss://nostr.p2sh.co +wss://nostr.thomascdnns.com +wss://nostream.simon.snowinning.com +wss://relay.nostr.blockhenge.com +wss://nostr.buythisdip.com +wss://nostrua.com +wss://relay.bigred.social +wss://lingoh.dev +wss://nostr.poster.place +wss://nostr.geekgalaxy.com +wss://oarnx6xdrq5mygfdrbmzsvh3is3holefpz2x4qwbopwcicwd63gcivid.onion +wss://relay.nostropolis.xyz +wss://nostream-production-ba43.up.railway.app +wss://nostr.sabross.xyz +wss://relay.nvote.co +wss://nostrati.com +wss://cloudnull.land +wss://nostr.frennet.xyz +wss://nostr.wine.com +wss://nostr.sactiontwo.org +wss://nostr.liberty.fans +wss://nostr.primz.org +wss://btc-italia.online +wss://homenode.local:4848 +wss://nostr.frennet.xyzl +wss://relay.roosoft.com +wss://rasca.asnubes.art +wss://nostr.bitcoin.sexanewlycre +wss://nostr.barf.bz +wss://nostr.middling.mydns.jp +wss://relay.xuzmail.com +wss://no-str.wnhefei.cn:28443 +wss://quirky-bunch-isubghsvoi26fbbt3n7o.wnext.app +wss://nostr.fennel.org:7000 +wss://nostr.0ne.day +wss://nostr.vpn1.codingmerc.com +wss://nostr.jacany.com +wss://nostream.lucas.snowinning.com +wss://relay.beta.fogtype.com +wss://nostr.zue.news +wss://nostream.madbean.snowinning.com +wss://nostr2.rbel.co +wss://relay.1bps.io +wss://nostream-relay-nostr.831.pp.ua +wss://zee-relay.fly.dev +wss://nostrrelay.geforcy.com +wss://relay.nostr.jhot.me +wss://nostr.itredneck.com +wss://nostr.h3y6e.com +wss://relay.bitcoiner.social +wss://hos.lol +wss://iris.to +wss://nostr-pub.senisol.dev +wss://nostr-pub.wellirder.net +wss://bitcoinforthe.lol +wss://relav.nostr.info +wss://3e32-200-229-144-129.ngrok.io +wss://nostr-relay.hzrd149.com +wss://nostr-world.h3z.jp +wss://nostr.thesamecat.io +wss://nostr.compile-error.net +wss://relayable.org +wss://mostra.milou.lol +wss://nproxy.cc +wss://nostr.bitmatk.io +wss://coracle.social +wss://umbrel.local:4848 +wss://nostr.ownbtc.online +wss://wss.nostrgram.co:444 +wss://nostr.minimue81.selfhost.co +wss://relay.current.fy +wss://nostream.nostr.parts +wss://nostr.zebede.cloud +wss://nostrwhoop.ph +wss://relay.nostrified.org +wss://nproxy.zerologin.co +wss://nostr.pcdkd.fyi +wss://relay.kongerik.et +wss://nostr.eden.land +wss://nostr.retroware.run.place +wss://relay.humanumest.social +wss://bhagos.org +wss://hushvault.ie +wss://nostream-production-9458.up.railway.app +wss://nostr.nakamotosatoshi.cf +wss://globals.fans +wss://nostr.cruncher.com +wss://nostr.global.fan +wss://relay.nostr.snblago.com +wss://nostr.nokotaro.com +wss://ostr-1.afarazit.eu +wss://relay.zhix.in +wss://stats.nostr.band +wss://nostr.fine +wss://vxlw4rlg7go34ol43g4gxbvfu4txdzjauquvnbptzwjflezs3vik55id.onion +wss://big.fist.black +wss://universe.nostrich.landlangzhlangja +wss://relay.plebz.space +wss://nostrich.land +wss://relay.mynostr.fun +wss://swiss-enigma.ch +wss://nostr1.current.fyi +wss://relay.atlas.nostr.land +wss://nostr.band +wss://n.wingu.se +wss://nostr.jmdtx.com +wss://nostrproxy-1.f7z.io +wss://nostr.ch +wss://roundrockbitcoiners.com +wss://nostr.sept.ml +wss://srelay.roli.social +wss://nostr.monostr.com +wss://nostr.dojotunnel.online +wss://nostrica.dojotunnel.online +wss://nostr.shadownode.org +wss://thes.ai +wss://rsslay.wss +wss://nostr.vol.io +wss://nostrgram.co +wss://habla.news +wss://runningnostr.lol +wss://mostr.pub +wss://relay.example2.com +wss://profiles.f7z.io +wss://nostr.adpo.co +wss://jp-relay-nostr.invr.chat +wss://nostr.anchel.nl +wss://mutinywallet.com +wss://relay.nostrbr.online +wss://filter.eden.nostr.land +wss://relay.nostrdocs.com +wss://relay.nostr.lucentlabs.co +wss://n.xmr.se +wss://nostr.relayer.rs +wss://monad.jb55.com:8080 +wss://nostr.watch +wss://universe.nostrich.landlangenlangzhlangja +wss://nostr.asdf.mx +wss://ts.relays.world +wss://arc1.arcadelabs.co +wss://stealth.wine +wss://nostr.bg +wss://really.nostr.bg +wss://realy.nostr.bg +wss://relai.nostr.bg +wss://relay-verified.deschooling.us +wss://4.up.railway.app +wss://nostr-relay.aapi.me +wss://nostr-z9tc.onrender.com +wss://nostrich.site +wss://nostr.ginuerzh.xyz +wss://310b-200-229-144-129.ngrok.io +wss://nostr.aste.co +wss://black.nostrcity.club +wss://nostr.guru +wss://nostrica.com +wss://relay.leesalminen.com +wss://nostr.shroomslab.net +wss://meta-relay-beta.nostr.wirednet.jp +wss://nostr.reamde.dev +wss://nostr.africa +wss://powrelay.xyz +wss://rsslay.data.haus +wss://nostr.danvergara.com +wss://nostr.one.re +wss://dev2.hazilitt.fiatjaf.com +wss://nostr-2.zebdeee.cloud +wss://zerosequioso.com +wss://brb.io.relay +wss://relay.realsearch.cc +wss://nodestr.fmt.wiz.biz +wss://r.alphaama.com +wss://nostr.relay-wlvs.space +wss://relay.21spiritis.io +wss://nostr.pinkanki.org +wss://nostr.damus.io +wss://fin-nostr.seekdisruption.com +wss://relay.nostr.lu.ke +wss://nostr.rdfried.com +wss://nostr.trustbtc.org +wss://nostr.verymad.net +wss://relay.damus.info +wss://relays.nostrplebs.com +wss://nostr.688.org +wss://15171031.688.org +wss://dgi4mb7antpcmrx4rynm6xq52xzt5duvxa4iwucq4mszgpz6smrjajqd.onion +wss://mastodon.cloud +wss://nostr.ch3n2k.com +wss://nostr.forecastdao.com +wss://nostr.nostrelay.org +wss://nostr.robotesc.ro +wss://nostr.test.aesyc.io +wss://nostr.web3infra.xyz +wss://nostrrelay.maciejz.net +wss://nostrsxz4lbwe-nostr.functions.fnc.fr-par.scw.cloud +wss://nostrpurple.com +wss://rsslay.ch3n2k.com +wss://nos.qghs.in +wss://nostr.nordlysln.net:3241 +wss://nostr.net.in +wss://relay.rip +wss://universe.nostrich.landlangenlangja +wss://wmv-vm.local:4848 +wss://relay.austritch.net +wss://relay.oxtr.dev +wss://rwlay.bigred.social +wss://relay.lexongtonbitcoin.org +wss://knostr.neutrine +wss://nostr.online +wss://filter.stealth.wine +wss://nostream-production-5895.up.railway.app +wss://nostr.stereosteve.com +wss://relay01.apus.network +wss://test.nostr.0x50.tech +wss://nostr.0x50.tech +wss://nostr.256k1.dev +wss://nostr.malin.onl +wss://jqiwgflfw4dezjsy42frompmknrlcfazoiyngftgknj7yrmnhtobd7id.local +wss://b.ayit.org +wss://nostrelay.rajabi.ca +wss://off20chain.pub +wss://nostr.milou.land +wss://nostr.primedomain.fr +wss://nostr.theblockreward.com +wss://anon.computer +wss://relay.hamnet.io +wss://nostramsterdam.vpx.moe +wss://global-relay.cesc.trade +wss://btcpay.kukks.org +wss://relay.cent2sat.com +wss://nostr.mnethome.de +wss://nostream.sh4.red +wss://klockenga.social +wss://nostream.megadope.snowinning.com +wss://nostr.cvilleblockchain.org +wss://nostr.bitocial.xyz +wss://nostr.bitcoiner.socail +wss://nostr-relay.eniehack.net +wss://greenart7c3.dedyn.io +wss://nostr.data.naus +wss://8.tcp.ngrok.io:19607 +wss://relay.nostr24.com +wss://d463rbo7dgbfuxvvxpory2og2etl4gttfzmqcixdq7rpts47lpgolkyd.onion +wss://dublin.saoirse.dev +wss://www.weixin.com +wss://nostr-rs-relay.cryptoassetssubledger.com +wss://nostr.kojira.net +wss://nostr.fan +wss://nostr.pk +wss://getalpy.com +wss://billert.xyz +wss://circle-ay.info +wss://jawsh.xyz +wss://walletofsatoshi.com +wss://ogblock.xyz +wss://nodestrich.com +wss://kunigaku.gith +wss://nostr.21ideas.org +wss://133332.xyz +wss://asats.io +wss://nostrchack.me +wss://chalow.net +wss://cashu.me +wss://tsukemonogit.git +wss://h3y6e.com +wss://elder.nostr.land +wss://xmr.rocks +wss://nostr.build +wss://mofumemo.com +wss://kpherox.dev +wss://sb.nostr.band +wss://tyiu.xyz +wss://welkinhere.githu +wss://ocha.one +wss://in.tips +wss://vitorpamplona.co +wss://fiatjaf.com +wss://akiomik.github.io +wss://stacker.news +wss://nvk.org +wss://lordkno.ws +wss://nostr.indus +wss://lotdkno.ws +wss://nosutora.com +wss://milou.lol +wss://mostr.pu +wss://dergigi.com +wss://shirehodl.com +wss://cash.app +wss://h3z.jp +wss://murachue.cytes.net +wss://snowcait.gith +wss://jb55.com +wss://nodeless.io +wss://orange-crush.com +wss://nostr.com.au +wss://oooxxx.ml +wss://plebs.place +wss://ahr999.com +wss://penpenpng.github.io +wss://nostrpurple.co +wss://thank.eu +wss://weep.jp +wss://tigerville.no +wss://nostrcheck.me +wss://frenstr.com +wss://ln.tips +wss://ryumu.dev +wss://honeyroad.store +wss://harlembitcoin.com +wss://f7z.io +wss://relay.fan +wss://nisshiee.org +wss://www.lopp.net +wss://getalby.com +wss://heguro.com +wss://wil.bio +wss://b.tc +wss://nodedttich.com +wss://relay.taldra.in +wss://relat.nostrica.com +wss://nostr.boring.surf +wss://nostr.raitisoja.net +wss://nostr.astrox.app +wss://nostr.mjex.me +wss://slick.mjex.me +wss://nostr.hrmb.org +wss://relay.semaphore.life +wss://rss.nostr.band +wss://63ragcfwb5xhoe5gfflazfyrde3qjdo73cblhhmnbviizowdo2q5haid.onion:5051 +wss://nostrblip.app +wss://relay.vanderwarker.family +wss://relay-local.cowdle.gg +wss://relay.damus.com +wss://relay.reeve.cn +wss://relay.strfry.net +wss://relay.alxgsv.com +wss://nostrv0l.io +wss://relay.nostrula.com +wss://release.nostr.band +wss://nostr.k3tan.com +wss://nostr.bitcoin.social +wss://thesimplekid.space +wss://relay.ypcloud.com +wss://at.nostrwork.at +wss://nostream.dev.kronkltd.net +wss://nostr-relay.xbytez.io +wss://relay.nostr.io +wss://relay.nvote.co:443 +wss://relay.cryptoculture.com +wss://rjj6ejkihilniytxs56qrgtttgcfnnjvbii6vaas6jzppcmekd63ugad.local +wss://puravida.nostr.land.com +wss://bitcoinmaximalist.online +wss://wine.nostr +wss://nostr.messagepush.io +wss://nostrich.love +wss://relaynostrplebs.com +wss://hamstr.to +wss://yosupp.app +wss://snort.social +wss://relay.nostr.gt +wss://nost.ratchat.nl +wss://nostr.chrissmith.site +wss://i.relay.boats +wss://nostr.wineto +wss://nostr.eluc.ch +wss://nostrplebs.com +wss://nostr.tools.global.id +wss://nostr.rocketnode.space +wss://relay.roli.social +wss://bitcoin.nostr.com +wss://relay.badgr.space +wss://nostriches.club +wss://nostr-check.me +wss://nostrelay.nokotaro.com +wss://rbr.bio +wss://rly.bopln.com +wss://6amyhf3sjvgxe5qzbx4xn52pcnqresdmi7szxurp6umkvz6mthxjdcad.onion +wss://6amyhf3sjvgxe5qzbx4xn52pcnqresdmi7szxurp6umkvz6mthxjdcad.local +wss://20nos.lol +wss://test.theglobalpersian.com +wss://nostr.exposed +wss://nostr-pub.liujiale.me +wss://nostream.frank.snowinning.com +wss://nstrs.fly.dev +wss://eospark.com +wss://relay.nosterplebs.com +wss://nproxy.kristapsk.lv +wss://nostr.universalname.space +wss://relays.snort.social +wss://nostr.how +wss://kukks.org +wss://nostr.dutch.cryptonews +wss://relay.cryptojournaal.net +wss://relay.dutch.cryptonews +wss://nostr.cryptojournaal.net +wss://no-str.wnhefei.cn +wss://www.131.me +wss://rsr.uyky.net +wss://nostr-relay.ie9.org +wss://nostr.fmt.wis.biz +wss://nostr.exotr.dev +wss://nostr.merrcurr.com +wss://realy.damus.io +wss://nerostr.xmr.rocks +wss://rkdgwzgvcgrciemlnfsxqgyrv5whgpw44s6zycokmuchpq4ucflgjtqd.local +wss://nostr.simplex.icu +wss://ralay.damus.io +wss://relay.kronkitd.net +wss://nostr.info +wss://nostr.lnnodeinsight.com +wss://nostr.truckenbucks.com +wss://brt.io +wss://nostr.lingoh.dev +wss://relay.nor.st +wss://nostr-relay.inmarkets.com +wss://wiz.biz +wss://nostream.git +wss://nostr.dpbu.de +wss://wcl2meyp236fa3dmfzfyq6aacbdoixrlocb6zozjs6xklxizschj2did.local +wss://relay.nostr.co.jp +wss://relap.orzv.workers.dev +wss://nostr.bitcoiner.socia +wss://eden.nosrt.land +wss://strfry.cryptocartel.social +wss://nostr.citizenry.technology +wss://universe.nostrich.landlang +wss://nostr.inprivate.network +wss://relay.snort.test +wss://lpkue6jtz3pnp7zok4jwlct4n3mzuffsfrpagiffsuplyaswhdtmpoid.local +wss://nostr.rezhajulio.id +wss://lnbits.eldamar.icu +wss://nostr.freefrom.fi +wss://yael.at +wss://rain8128.github.io +wss://badges.page +wss://latam1-nostr.stealthy.co +wss://nostr.relay.se +wss://nfdn.testnet.dotalgo.io +wss://relav.nostr3.io +wss://ofchain.pub +wss://nostr.fmt.wiz.bi +wss://relay.gui.dog +wss://eden.nostry.land +wss://nostr.relay.limo +wss://relay.nostrichs.org +wss://20nostr.mom +wss://relay20damus.io +wss://nostr.sandwich.pro +wss://sq.qemura.xyz +wss://nostr-pub.seminol.dev +wss://nostr.eunundzwanzig.space +wss://nostr.bitcoinet.social +wss://nostr-relay.untether.me +wss://relay.nostr.pub +wss://nostr.100p.org +wss://7tdom3xuus7ekv423ul46w3j43zyixjj54yoe62bndpcrgii3adeppid.local +wss://nostr.gram +wss://nostr.videre.net +wss://nostr-2.zebedee.cloudwss +wss://nostr-pub.wellorder.netwss +wss://sonzai.net +wss://snort.fail +wss://ppavybjpqjft5slnpeovehbegomhwtvvxtesvwwdrfndz6qe2c5kf2ad.local +wss://nostr.land +wss://relay.nvote.com +wss://purevida.nostr.land +wss://nostr.bitcoiner.soical +wss://nostr.inosta.co +wss://nas.lol +wss://wss.nostr.milou +wss://nostr.cheesebot.org +wss://eyeswideshut.ath.cx +wss://test.relays.world +wss://relay.snort.com +wss://relay.nostrplebs.co +wss://nostr.kimi.im +wss://nostrum.com +wss://wss.nostr.wine +wss://relay.usenostr.org +wss://paladium.my:4848 +wss://umbrel.home.local:4848 +wss://nostr.metamadeenah.com +wss://lnbits.sdbtc.org +wss://relay.nostr.mutinywallet.com +wss://relay.theglobalpersian.com +wss://relay1.easymeta.app +wss://relay.nostris.online +wss://s1.wonder3.org +wss://eden.nostraland +wss://byc.klendazu.com +wss://ephemerelay.mostr.pub +wss://7si6co27cvaw5yjyx6asvxfmaw5ah2arywwgrem4y5svi5ntskoeb5id.onion +wss://nwmdev.com +wss://nostro.online +wss://nostr.massimux.com +wss://nostr.glate.ch +wss://nostr.openordex.org +wss://nostr.schorsch.fans +wss://nostr-relay-dev.nisshiee.org +wss://ibz.me +wss://alexandernostrplebs.com +wss://fishbanananostrplebs.com +wss://relay.nostrcheck.com +wss://nostr.roli.io +wss://nostr.net.za +wss://nostr.worldkey.io +wss://nostr-pub.welloorder.net +wss://nostr.actin.io +wss://nostrzebedee.cloud +wss://nostr.zclub.app +wss://nostr.13x.sh +wss://nostr.totient.xyz +wss://nostr1.federated.computer +wss://nostr.zhix.in +wss://nostr.vdstruis.com +wss://caro-relay.fiatjaf.com +wss://nostr.winewss +wss://notstro.wine +wss://nostr.btc-library.com +wss://nostr.phenomenon.space +wss://nostr.octr.dev +wss://nostr.impervious.live +wss://nostr.plebs.space +wss://iefan.tech +wss://w3ird.tech +wss://yunginter.net +wss://nostr.coach +wss://sleepy.cafe +wss://freespeechextremist.com +wss://nostr.uselessshit.com +wss://liberdon.com +wss://eveningzoo.club +wss://gleasonator.com +wss://mindly.social +wss://ottawa.place +wss://noagendasocial.com +wss://poa.st +wss://misskey.io +wss://misskey.cf +wss://nostr.bybieyang.com +wss://mastodon.online +wss://toad.social +wss://best-friends.chat +wss://universeodon.com +wss://mastodon.world +wss://mastodon.social +wss://nostr.vulpem +wss://nicecrew.digital +wss://front-end.social +wss://nostr-relay.wellorder.net +wss://relays.pro +wss://rsslay.sovbit.host +wss://seal.cafe +wss://social.6bq.de +wss://universe.nostrich.landlangjalangzh +wss://social.xenofem.me +wss://mi.hibi-tsumo.com +wss://mstdn.social +wss://mas.to +wss://relay.rebelbase.site +wss://pixelfed.social +wss://digitalcourage.social +wss://ruby.social +wss://cr8r.gg +wss://nijimiss.moe +wss://returtle.com +wss://lor.sh +wss://toot.community +wss://mstdn.jp +wss://relay.berserker.town +wss://nostr.rajabi.ca +wss://fosstodon.org +wss://misskey.takehi.to +wss://izj3isbk3pmade74ontdijodhehsytnw2iokdhh6k3flk4mq2pau6sid.onion +wss://mastodon.scot +wss://nostriches.org +wss://aus.social +wss://relay.nostr.amane.moe +wss://romancelandia.club +wss://stonez.me +wss://merrcurr.com +wss://nostr.ist +wss://onprem.wtf +wss://fedibird.com +wss://s2.wonder3.org +wss://freespech.casa +wss://disabled.social +wss://relay.house +wss://nostr-pub.welllorder.net +wss://social.teamb.space +wss://infosec.exchange +wss://blockedur.mom +wss://progressivecafe.social +wss://mstdn.ca +wss://c.im +wss://mefi.social +wss://basebitcoinplebs.place +wss://med-mastodon.com +wss://ohai.social +wss://defcon.social +wss://pxlmo.com +wss://zlocur7ctbds4qsdswb3qpkp6n2e2ywqne2fp2tdn4fqubpudfiyxwid.local +wss://thebag.social +wss://mstdn.party +wss://relay.serpae.xyz +wss://clew.lol +wss://qoto.org +wss://relay.exchange +wss://nostr.relayable.org +wss://mastodon.coffee +wss://kmy.blue +wss://home.social +wss://detmi.social +wss://brighteon.social +wss://nostr.nom +wss://theblower.au +wss://astral.nostr.land +wss://beige.party +wss://orangepill.dev +wss://redgreenblue.click +wss://nostr.fredix.xyz +wss://nostr.essydns.ca +wss://nostr.private.network +wss://nostr.member.cas +wss://nostr-rely.digitalmob.ro +wss://relay.nostr +wss://relay.current +wss://no-str.or +wss://pl.gamers.exposed +wss://studentchadpolytechnic.com +wss://scicomm.xyz +wss://relay.alien-sos.gov +wss://masto.es +wss://spinster.xyz +wss://parallels-parallels-virtual-platform.local:4848 +wss://relay.daums.io +wss://kolektiva.social +wss://mastodonapp.uk +wss://convo.casa +wss://sfba.social +wss://techhub.social +wss://leafposter.club +wss://nrw.social +wss://mastodon.uno +wss://handon.club +wss://social.vivaldi.net +wss://nostr.goller.net +wss://minazukey.uk +wss://mstdn.beer +wss://expressional.social +wss://paid.nostr.0x50.tech +wss://mstdn.nere9.help +wss://relay.nostr.ai +wss://relay.noswss +wss://relay.nwss +wss://furry.engineer +wss://uselessshit.co +wss://kosmos.social +wss://mynostr.io +wss://premis.one +wss://social.tchncs.de +wss://retro.pizza +wss://pearl-mount-showed-fishing.trycloudflare.com +wss://kpa4k6acxzjv2m2p72keftbpaymwpq2h67jqnin3d4y3djxyheuifoqd.onion +wss://gm7.social +wss://relays.nostr.info +wss://chitter.xyz +wss://hackers.town +wss://anarchism.space +wss://relay.nostrica +wss://halifaxsocial.ca +wss://asimon.org +wss://nostr.blipme.add +wss://troet.cafe +wss://octodon.social +wss://m.cmx.im +wss://filename-ambassador-distance-mountains.trycloudflare.com +wss://nostr.getgle.org +wss://relay.froth.zone +wss://novoa.nagoya +wss://relay.dispute.systems +wss://clubcyberia.co +wss://thechimp.zone +wss://coolsite.win +wss://puravida.nostra.land +wss://shelter.local:1111 +wss://det.social +wss://nostr-2.zebee.cloud +wss://chaosfem.tw +wss://nostr.v01.io +wss://social.kechpaja.com +wss://mastodon.green +wss://plebchain.nostr +wss://plebchain.nostr.land +wss://relay.onsats.org +wss://u5epuanp2fbie4phw6zekzna6zotvsffji4td4ee7iwgwdxlz4kwqqad.onion:5051 +wss://3ddc8bbee6db.ngrok.app +wss://b4968f09859e.ngrok.io +wss://genserver.social +wss://masto.ai +wss://hachyderm.io +wss://shitpost.cloud +wss://oldbytes.space +wss://mastodon.ie +wss://baraag.net +wss://nostr.dvdt.dev +wss://relay.com.de +wss://nostr.rbel.co +wss://bologna.one +wss://nostr-relay.app +wss://kagamisskey.com +wss://nostr-rs-relay.phamthanh.me +wss://nostr.bcmp.com +wss://blogstack.io +wss://rly.nostrkid.com +wss://mastodon.bida.im +wss://freeatlantis.com +wss://pieville.net +wss://climatejustice.rocks +wss://relay.mynostr.id +wss://relay.farscapian +wss://relay.blogstack.io +wss://orwell.fun +wss://misskey.04.si +wss://sushi.ski +wss://nostr.milou.lo +wss://pawoo.net +wss://tooter.social +wss://mastodon.sdf.org +wss://nostr.com +wss://misskey.design +wss://macaw.social +wss://relay.nostr.inforelay.nostr.band +wss://pub1.southflorida.ninja +wss://strawberry-pudding.net +wss://mastodon-japan.net +wss://nostream.0x50.tech +wss://multiplextr.coracle.social +wss://pleroma.skyshanty.xyz +wss://uxxq6b2enojvflhkrzsg4erakd5rrb7v2cql4m4pspj4xtqwyli47rid.local +wss://masto.deoan.org +wss://replay.damus.io +wss://melhorque.com.br +wss://nostr1.actn.io +wss://nostr.bolt.fun +wss://relay.vtbmoyu.com +wss://eosla.comrelay.zeh.app +wss://bikeshed.party +wss://nostr.xanny.family +wss://milker.cafe +wss://nostr.give.africa +wss://relay.got-relayed.com +wss://cum.salon +wss://puravid.nostr.land +wss://soc.punktrash.club +wss://seafoam.space +wss://h4.io +wss://nostr.swiss.enigma.ch +wss://toot.cafe +wss://sneed.social +wss://newsie.social +wss://indg.club +wss://xoxo.zone +wss://relay.nostr.bitcoiner.social +wss://relay.snort.band +wss://socel.net +wss://social.coop +wss://postpandemicparty.org +wss://merveilles.town +wss://search.nostr.wine +wss://search.nos.today +wss://mstdn-huahin.com +wss://eden.nostr.la +wss://nostrja-kari-nip50.heguro.com +wss://relay.gems.xyz +wss://plnetwork.xyz +wss://nostr.swiss-enigma.sh +wss://geofront.rocks +wss://toot.io +wss://indieweb.social +wss://mastodon.content.town +wss://awaymessage.club +wss://relay.intify.io +wss://mstdn.science +wss://maniakey.com +wss://otadon.com +wss://mstdn.guru +wss://misskey.noellabo.jp +wss://hessen.social +wss://mastodon.top +wss://ipv6.nostr.wirednet.jp +wss://relay.nostr-relay.org +wss://cum.camp +wss://nebbia.fail +wss://current.fyi +wss://bae.st +wss://lolison.network +wss://ioc.exchange +wss://bylines.social +wss://decayable.ink +wss://nostr.unitedserializer.com +wss://urbanists.social +wss://dark-elves.social +wss://writing.exchange +wss://nostr.rikmeijer.nl +wss://misskey.social +wss://ht.nixre.net +wss://mathstodon.xyz +wss://t7jvqwu35hneszx7fihsprbcpwonlcfnsjr4xtn6shqgwbv324w4gdid.local +wss://abla.news +wss://muenchen.social +wss://homeserver.drake-carp.ts.net:4848 +wss://snailedit.social +wss://mastodon.gamedev.place +wss://tech.lgbt +wss://mast.lat +wss://econtwitter.net +wss://veganism.social +wss://btclolap6mm4tl37huslk6j76enq7qxaj2kwq7w6cdr5ros56eletcqd.onion +wss://toot.cat +wss://nostr.zenon.info +wss://misskey.art +wss://nostr.hushvault.ie:4848 +wss://o6ga6lxnax2z7pgkkenifollohkrv55r36mdzdtofi7d5yyif2f4o5yd.onion:5051 +wss://20nostr-pub.wellorder.net +wss://ecoevo.social +wss://nostr.zerofiat.world +wss://nostr.atitlan.io +wss://detroitriotcity.com +wss://rollenspiel.social +wss://paquita.masto.host +wss://literatur.social +wss://dave.st.germa.in +wss://sunny.garden +wss://nostrpro.xyz +wss://relay.getalby.com +wss://misskey.systems +wss://mamot.fr +wss://social.anoxinon.de +wss://relay.nostr.social +wss://mindmachine.org +wss://microblog.club +wss://relay.utxo.com +wss://universe.nostrich.landlangth +wss://social.teci.world +wss://social.freetalklive.com +wss://mk.absturztau.be +wss://social.ornella.xyz +wss://left-tusk.com +wss://bofh.social +wss://a11y.social +wss://shroomslab.net +wss://relay2.nostr.vet +wss://mugicha.club +wss://laserbeak.local:4848 +wss://annihilation.social +wss://humble.cafe +wss://nostr.relay.damus.io +wss://nostr.milol.lol +wss://nostr.blimpme.app +wss://noc.social +wss://wetdry.world +wss://nostr.taxi +wss://nostr.21-bitcoin.org +wss://relay.llevotu-bitcoiners.info +wss://pl.kitsunemimi.club +wss://djsumdog.com +wss://citadel.local:4848 +wss://federate.blogpocket.com +wss://chaos.social +wss://mastodon.me.uk +wss://oxtr.dev +wss://misskey.cloud +wss://varishangout.net +wss://lacosanostr.com +wss://willem.currycash.net:4848 +wss://lightniningrelay.com +wss://queer.party +wss://lightning.relay.com +wss://mastodon.nl +wss://purplepag.es +wss://cupoftea.social +wss://bitcoiner.socialwss +wss://relay.current.io +wss://sigmoid.social +wss://wandering.shop +wss://braydmedia.de +wss://quey.la +wss://nostr.zebeedee.cloud +wss://nostr-2.zebeedee.cloud +wss://artsio.com +wss://nostr1676031941328.app.runonflux.io +wss://arsip.ddns.net +wss://relay.nostrcitadel.org +wss://relay.nostr-citadel.org +wss://nostr-citadel.org +wss://blastr.f7z.io +wss://nostr.myowndamnnode.com +wss://snowdin.town +wss://nostr.zxcvbn.space +wss://relay.notmandatory.org +wss://bitcoin.social +wss://friendsofdesoto.social +wss://zirk.us +wss://digipres.club +wss://nostr-test.elastos.io +wss://nostr.onsat.org +wss://damus.relay.io +wss://beefyboys.win +wss://nostrija-kari.heguro.com +wss://froth.zone +wss://ns.penseer.com +wss://relay.nostrical.com +wss://nostr.hoshizora.ch +wss://kunigaku.github.io +wss://nostr.shino3.net +wss://umbrel.tailbb128.ts.net:4848 +wss://tailbb128.ts.net:4848 +wss://fedi.twoshortplanks.com +wss://filter.nostr.winebroadcasttrue +wss://nostr.doufu-tech.com +wss://relay.hodl.haus +wss://pgh.social +wss://coeditor-congested.fractalnetworks.co +wss://mastodon.podaboutli.st +wss://frenfiverse.net +wss://nostr.f4255529.fun +wss://nostream.megadope.snowinning +wss://kiritan.work +wss://forever21.lol +wss://zitron.net +wss://mastodon-swiss.org +wss://relay1.current.fyi +wss://nostr.plebs.com +wss://multiplextr.corocal.social +wss://nostr.zedebee.cloud +wss://nostr.mutinywallet.comisntbanned.addthatonesotheycangetnotesrelayedtotherestofthenetworkfrominsideofchina +wss://mastodon.mit.edu +wss://relaynostrati.com +wss://relaynostr.band +wss://relaynostr.info +wss://relaychenxixian.cn +wss://relaysnort.social +wss://relaynostr.com.au +wss://nostr-tbd.website +wss://relay.hoshizora.ch +wss://social.balsillie.net +wss://strangeobject.space +wss://relay.nvote20.co +wss://fla.red +wss://urusai.social +wss://chad.polytechnic.com +wss://robo358.com +wss://conxole.io +wss://relay.ohbe.me +wss://nostr.bitcoiner.com +wss://www.mutinywallet.com +wss://x.9600.link:10070 +wss://a11y.info +wss://homelab.host +wss://k65qz57zx4sw24fow2bvchjgmpjqljj4cp3oa7dtpbbprjifsdotggid.local +wss://relay.vtuber.directory +wss://x.9600.link:8000 +wss://nostr.plebs.win +wss://renkontu.com +wss://nost.massmux.com +wss://submarin.online +wss://social.librem.one +wss://relai.kongerik.et +wss://mstdn.io +wss://astral.swiss-enigma.ch +wss://stereophonic.space +wss://relayer.pleb.social +wss://mastodon.nz +wss://nostr.nostr.de +wss://nostr.thezap.club +wss://proxy.shroomslab.net +wss://pfr24mrpxowclhm4y6adu36kbo3erx7gskzyfqqhnhfpdkdmylpfgkyd.local +wss://nostr.hodl.haus +wss://vtdon.com +wss://relay.nost.band +wss://fnxwipsg3lfzij64lvjgmutvkkpd7eo2mr2khxkofyywf3vsvbk73jad.onion +wss://fnxwipsg3lfzij64lvjgmutvkkpd7eo2mr2khxkofyywf3vsvbk73jad.local +wss://md.hugo.nostr +wss://boks.moe +wss://truthsocial.co.in +wss://mastodon.xyz +wss://relay.universalname.space +wss://relay-nostr.wirednet.jp +wss://fedi.pawlicker.com +wss://favcalc.com +wss://rot13maxi.com +wss://awayuki.net +wss://mazinkhoury.com +wss://g0v.social +wss://shpposter.club +wss://a.lufimianet.jp +wss://pura20vida.nostr.land +wss://sotalive.net +wss://sendsats.lol +wss://vida.page +wss://wagvwfrdrikrqzp7h3b5lwl6btyuttu7mqpeji35ljzq36ovzgjhsfqd.onion +wss://uec2cmjauzufrtlq6wq6l2ujfncdvo3suezz423gsvz5xvhehm2mcgid.onion +wss://mastodon-belgium.be +wss://rejecttheframe.xyz +wss://nogood.store +wss://getaiby.com +wss://plebchain.club +wss://nostrverified.com +wss://x.9600.link +wss://n0p0.shroomslab.net +wss://orangemakura.xyz +wss://jamw.net +wss://7ab7qqbj2dw3pjnkoskgsfn4ikqc7orwnkpmcfjmeobw63kf4zgykjid.local +wss://snabelen.no +wss://floss.social +wss://mastoot.fr +wss://kmc-nostr.amiunderwater.com +wss://hodl.camp +wss://xmr.usenostr.com +wss://nostr-pub.wellorn.net +wss://gudako.net +wss://ryona.agency +wss://kafeneio.social +wss://massmux.com +wss://freezepeach.online +wss://nostream-production-f83d.up.railway.app +wss://taobox.pub +wss://mastodon.radio +wss://einundzwanzig.relay.com +wss://journa.host +wss://social.here.blue +wss://toot.wales +wss://staging.nostr.com.se +wss://pleroma.soykaf.com +wss://berserker.town +wss://zapforart.site +wss://forall.social +wss://nostrja-kari.heguro.comyee +wss://higheredweb.social +wss://social.gnuhacker.org +wss://the.hodl.haus +wss://ng4jk6yiqgfczo4wyxszuj7w6jok3fptehu533o3mlzs3vph3dvjfdid.onion +wss://nostr.mtpx.ovh +wss://relay.coollamer.com +wss://glasgow.social +wss://fedi.absturztau.be +wss://nostr.frostr.xyz +wss://relay.runningnostr.lol +wss://relay2.vtuber.directory +wss://nerdculture.de +wss://nostr-relay.untethr.meoperator +wss://nostr.verif-slothy.win +wss://nostr.pub +wss://social.process-one.net +wss://invillage-outvillage.com +wss://otofu.uk +wss://universe.nostrich.landlangenlangpt +wss://mastodon.iriseden.eu +wss://frighteningdeafeningagent.nailuogg.repl.co +wss://obo.sh +wss://relay.hodlhaus.net +wss://rdrama.cc +wss://nostr.packetlostandfound.us +wss://test.relay +wss://mastodonbooks.net +wss://southflorida.ninja +wss://blob.cat +wss://tooting.ch +wss://relay.pineapple.pizza +wss://relay.nostr.directory +wss://relar.nostr.bg +wss://reisen.church +wss://relay.honk.pub +wss://nostr.rsfriedl.com +wss://relay.nort.social +wss://1611.social +wss://the.hodl.house +wss://relayable.com +wss://fedi.syspxl.xyz +wss://nostrpub.yeghro.site +wss://skr5bbrgzfnideglw4cs2iw6au2jm2b7gupocxbmkv5qopo6rcitmiqd.local +wss://a2mi.social +wss://status.relayable.org +wss://toot.blue +wss://btcqspp5dl4rlgl5pomcyv3odfeki7a5zrmjoekyu5vsoqz5bth4e7yd.onion +wss://bitcoinr6de5lkvx4tpwdmzrdfdpla5sya2afwpcabjup2xpi5dulbad.onion +wss://7tdom3xuus7ekv423ul46w3j43zyixjj54yoe62bndpcrgii3adeppid.onion +wss://dnze4ekho2kuiejwatjw5omeprtmdaum2ukok52roiu5rztii3rp2aid.onion +wss://53snncs7vegargpaardbxjnii2oan3xpmbeaf6czwoqa2axz5mvbsjid.onion +wss://fnqdhz3df33da6wxg7jskvumd5rjn3nknln6ecun7uwwysc7vkwkjgid.onion +wss://relay.thefockinfury.wtf +wss://silliness.observer +wss://freak.university +wss://piaille.fr +wss://nostr.weking.tk +wss://f.reun.de +wss://radixrat.com +wss://hostux.social +wss://chat.freenode.net +wss://no.str..cr +wss://nostr.inoata.cc +wss://kappa.seijin.jp +wss://floyds.io +wss://mast.dragon-fly.club +wss://zbd.ai +wss://misc.name +wss://pdx.social +wss://0w0.is +wss://d6jvu2tev2rblkuzgu4ydw2413jizr53j26ut47hxpykvtusbvekhiid.onion +wss://d6jvu2tcv2rblkuzgu4ydw24l3jizr53j26ut47hxpykvtusbvekhiid.onion +wss://drcassone.social +wss://mastodon.energy +wss://rayci.st +wss://ischool.social +wss://nostr.halfway2forever.com +wss://relay.nostr.rocks +wss://nostr.stoner.com +wss://2nodez.com +wss://rs.nostr-x.com +wss://schleuss.online +wss://thrashzone.org +wss://relay.nostrgraph.com +wss://relay.2nodez.com +wss://occult-zuki.com +wss://mastodon.lithium03.info +wss://bigbadpc.local:4848 +wss://social.gr0k.net +wss://nostr.wirednet.jp +wss://relay.plebster.com +wss://bitcoin.nostr +wss://mastodon.im +wss://brb..io +wss://nostr.sandwhich.farm +wss://relay.nos.lol +wss://beta.nostr.v0l.io +wss://eden.nostr.space +wss://powerlay.xyz +wss://rap.social +wss://relay.mutinywallet.com +wss://rogue.earth +wss://600.wtf +wss://klabo.blog +wss://petrikajander.com +wss://tgkzmdd.help +wss://nostr.red +wss://brb.lol +wss://jz2l2bf6f6wssdqwkg7ogthkc5i3ymyiwkaz3tbhff6ro3h3zqddekyd.onion +wss://mstdn.mini4wd-engineer.com +wss://nostr.exposd +wss://nostr.a-ef.org +wss://computerfairi.es +wss://social.targaryen.house +wss://o3o.ca +wss://walkah.social +wss://cosocial.ca +wss://filter.nostr.band +wss://relais.nostrview.com +wss://gigaohm.bio +wss://dobbs.town +wss://bark.lgbt +wss://mastodon.gal +wss://snug.moe +wss://genomic.social +wss://relay.orangepilldev.com +wss://social.sdf.org +wss://social.camph.net +wss://mstdn.poyo.me +wss://nein.lol +wss://nostr.i00.org +wss://kemono.ink +wss://mu.zaitcev.nu +wss://libera.site +wss://ca.hibi-tsumo.com +wss://social.bund.de +wss://xscape.top +wss://social.lol +wss://birds.town +wss://arnostr.com +wss://nostr.33co.de +wss://relay.nostr.lighting +wss://metadata-contacts-relays.pages.dev +wss://webs.node9.org +wss://pleroma.elementality.org +wss://suya.place +wss://livellosegreto.it +wss://peoplemaking.games +wss://nattois.life +wss://typo.social +wss://neutrine.com +wss://ragner-relay.com +wss://wss.nostr.uselessshit.co +wss://wss.nostrue.com +wss://relay.nvote.co:433 +wss://disobey.net +wss://rneetup.com +wss://arnostr.com:8433 +wss://relay.uxto.one +wss://relay.hackerman.pro +wss://thisis.mylegendary.quest +wss://poliversity.it +wss://sats.lnaddy.com +wss://rs2.abaiba.top +wss://rs1.abaiba.top +wss://rs2.abaiba.top.abaiba.top +wss://social.matarillo.com +wss://nostr01.counterclockwise.io +wss://backup.local:4848 +wss://touhou.vodka +wss://mi-wo.site +wss://nostr.f7z.io +wss://alive.bar +wss://strfry.nostr-x.com +wss://mastodontti.fi +wss://nostr.wellorder.net +wss://y.9600.link:8000 +wss://ephemrelay.mostr.pub +wss://byc-italia.online +wss://fissionator.com +wss://stranger.social +wss://eupolicy.social +wss://nostr-desktop.local:4848 +wss://aoir.social +wss://mstdn.plus +wss://nostrproxy.io:3333 +wss://mastodon.hams.social +wss://jorts.horse +wss://metalhead.club +wss://dice.camp +wss://mstdn.y-zu.org +wss://loffchain.pub +wss://mastodon.llarian.net +wss://nostr-relay2.thefockinfury.wtf +wss://2g2jzcfgq5lcrceuq23lmya2drm3ku5qmqimr3bvu3amol55vidctrad.onion +wss://mastodon.au +wss://bgme.me +wss://nostr.badran.xyz +wss://nostr.coincreek.com +wss://nostream-test.up.railway.app +wss://relay.blackthunder.click +wss://relay.grorp.com +wss://atomicpoet.org +wss://iddqd.social +wss://gusto.masto.host +wss://lifehack.social +wss://blorbo.social +wss://freecumextremist.com +wss://13bells.com +wss://rsslay.nostr.netrelay +wss://relay.zerosequioso.com +wss://nauka-relay.herokuapp.com +wss://nostr.nofdeofsven.com +wss://out.of.milk +wss://cawfee.club +wss://r.relay.fan +wss://alo.ottonove891.cf +wss://keinoha.tailnet-0240.ts.net +wss://nostr.paralelnipolis.cz +wss://tuiter.rocks +wss://elizur.me +wss://nostr-dev.newstr.io +wss://discuss.systems +wss://blahaj.zone +wss://mastodon.art +wss://makersocial.online +wss://gamepad.club +wss://nostr.flameofsoul.ru +wss://dmv.community +wss://relay.nostr.com +wss://nostr-desktop.saiga-shark.ts.net:4848 +wss://nostrfoxden.ddns.net:4848 +wss://soc.umrath.net +wss://ravenation.club +wss://oisaur.com +wss://nostr-relay.net +wss://social.mikutter.hachune.net +wss://lewacki.space +wss://fediscience.org +wss://todon.eu +wss://nuccy-nuc7i5bnk.local:4848 +wss://games.gamertron.net:4848 +wss://fiedlerfamily.net +wss://postnstuffds.lol +wss://nostr.planetary.social +wss://worldkey.io +wss://hcommons.social +wss://gymp7qquljs47xbbvs47hkptnyyzegy2jkst26mkjxaciifqffjatqid.onion +wss://rs3.abaiba.top +wss://sudo-nostr.com +wss://satgag.site +wss://nostr.lnbitcoin.cz +wss://relay20nostrplebs.com +wss://2pbkpndvpeebljfvjew6auq63lndzszqnntct5aqfmazslerzxe75kad.onion +wss://dragonchat.org +wss://welcome.nostr.wine +wss://relay.nostr.land +wss://social.linux.pizza +wss://dnppj4kopczovvzvpzmihv2iwe5wt3gbrxjnltjc2zdjpttrdz4owpad.onion +wss://potofu.me +wss://nostrbr.online +wss://mastorol.es +wss://notebook.taild34d0.ts.net +wss://t.aqn.jp +wss://nostr.mutinywallet +wss://wcone.nostr.wine +wss://norden.social +wss://eostagram.com +wss://shigusegubu.club +wss://toot.jkiviluoto.fi +wss://kiwifarms.cc +wss://swiss-talk.net +wss://v532btfg2fb4za2g476a7w23pgpkllc7uq274wqtktwjogt5ynb3ukqd.local +wss://mstdn.maud.io +wss://arc1.arcadelabs.com +wss://nostr.jp +wss://relay-jp.nostr.wirrdnet.jp +wss://climatejustice.social +wss://witter.cz +wss://mastodon.pnpde.social +wss://ttrpg-hangout.social +wss://beehaw.org +wss://thecanadian.social +wss://nostr.fbxl.net +wss://relay.sandwich.farm +wss://nostr.olwe.link +wss://botsin.space +wss://zeroes.ca +wss://photog.social +wss://paid.nostr.lc +wss://free.nostr.lc +wss://test.nostr.lc +wss://gzanlkgurj7zd3psqms3da4vrw4imurnyyzaycfuiiug7elqow7xlayd.onion:5051 +wss://masto.nu +wss://mastodon.uy +wss://bit.relay.center +wss://offchain.relay.center +wss://damus.relay.center +wss://wine.relay.center +wss://eden.relay.center +wss://moth.social +wss://nostr.masmux.com +wss://chrome.pl +wss://mastodon.ktachibana.party +wss://ak.kawen.space +wss://mementomori.social +wss://relay.s3x.social +wss://lnbits.michaelantonfischer.com +wss://yof23ggqmert72c5wcl5qglphapy3o2xjdedtkbrn2dt5rbae2s7f6qd.onion +wss://relay.snort.socail +wss://post.lurk.org +wss://yiff.life +wss://q3zaylwjjhq77yzx34lbydz26szzjljberwetkjgxgsapcekrpjzsmqd.onion +wss://lnbits.b1tco1n.org +wss://welcome.nostr.relay +wss://sound-money-relay.denizenid.com +wss://carnivore-diet-relay.denizenid.com +wss://africa.nostr.joburg +wss://nostr.jolt.run +wss://nostr.chainbits.co.uk +wss://ithurtswhenip.ee +wss://nostr.cloudversia.com +wss://relay1.east.us.nostr.btron.io +wss://ca.orangepill.dev +wss://pdx.land +wss://linh.social +wss://okla.social +wss://androiddev.social +wss://spore.social +wss://mastodo.fi +wss://kabedon.space +wss://nost.inosta.cc +wss://relay2cdamus.io +wss://nostr.openhoofd.nl +wss://dragonscave.space +wss://genart.social +wss://dewp.space +wss://layer8.space +wss://qou7zzll2mxx2ehl73n6pptmhizl5b3entowljlin3sqhcvltxdtlmad.onion:5051 +wss://nostr.wines +wss://relay.snort.relay.ryzizub.com +wss://pixelfed.de +wss://nostr.holyscapegoat.com +wss://nostr.einunzwanzig.space +wss://nostr.hifish.org +wss://colearn.social +wss://topspicy.social +wss://mastodon.neat.computer +wss://relay.nostr.hach.re +wss://nostr.dakukitsune.ca +wss://7ab7qqbj2dw3pjnkoskgsfn4ikqc7orwnkpmcfjmeobw63kf4zgykjid.onion +wss://esq.social +wss://famichiki.jp +wss://tribe.net +wss://masto.nobigtech.es +wss://umbrel-nuc.local:4848 +wss://mastodon.cocoasamurai.social +wss://debian.taildd32b.ts.net:4848 +wss://vlt.ge +wss://relay.johnnyasantos.com +wss://snort.relay.center +wss://nb.relay.center +wss://waag.social +wss://concentrical.com +wss://stat.rocks +wss://oransns.com +wss://relay-jpp.nostr.wirednet.jp +wss://oc.todon.fr +wss://jundow.gitlab.io +wss://neurodifferent.me +wss://jazztodon.com +wss://nostrich.friendship +wss://indieauthors.social +wss://werunbtc.com +wss://frogtalk.lol +wss://pop-os.local:4848 +wss://fediver.de +wss://d6qxo55dhms6revgrmbindvb5ejd3gw5hrji7ylkm6khghii3hjs3uyd.onion +wss://eldritch.cafe +wss://karlsruhe-social.de +wss://social.yl.ms +wss://nostr.mycloudhouse.duckdns.org +wss://nostr.otc.sh +wss://nya.social +wss://relay2.nostrchat.io +wss://relay1.nostrchat.io +wss://nostrja-world-relays-test.heguro.com +wss://ndk-relay.local +wss://reespeech.casa +wss://pleroma.atyh.cc +wss://lawfedi.blue +wss://akkoma.jasminetea.uk +wss://peeledoffmy.skin +wss://plush.city +wss://astrodon.social +wss://samenet.social +wss://toot.bike +wss://mi.yukioke.com +wss://social.growyourown.services +wss://mastodon.nu +wss://lou.lt +wss://functional.cafe +wss://relaydamus.io +wss://coma.social +wss://social.fbxl.net +wss://biplus.social +wss://toots.matapacos.dog +wss://psychoet.ml:3250 +wss://danserver.equipment +wss://nostr.lacrypta.com.ar +wss://wonkodon.com +wss://nostr.seankibler.com +wss://autistics.life +wss://cambrian.social +wss://rvqkqr5kl3dvvxyn67rfowcnvoflx4zby5tjbysavym4ycckti4dbjyd.onion +wss://swiss.nostr.lc +wss://snac.saifulh.online +wss://loma.ml +wss://nostr.privoxy.io +wss://366.koyomi.online +wss://mastodon.stormy178.com +wss://podcastindex.social +wss://bitcoiner.socia +wss://fedi.ml +wss://replayable.org +wss://nostr.schroomslab.net +wss://nostr.global.fans +wss://relay.weedstr.net +wss://vocalodon.net +wss://relay.nostr.bandadd +wss://nostr.oxtr.devadd +wss://jarvis.taild68e2.ts.net:4848 +wss://t7jvqwu35hneszx7fihsprbcpwonlcfnsjr4xtn6shqgwbv324w4gdid.onion +wss://stonez.local:4848 +wss://notrustverify.ch +wss://woof.group +wss://mastodon.floe.earth +wss://lnbits.plebtag.com +wss://kpop.social +wss://relay.wavlake.com +wss://mastodon.sharma.io +wss://travelpandas.fr +wss://alphapanda.prowss +wss://relay.saes.io +wss://barelysocial.org +wss://masto.komintern.work +wss://norcal.social +wss://nostr.zbd.gg +wss://mk.outv.im +wss://mstdn.mx +wss://col.social +wss://nostr.freedom.fi +wss://filter.nostr.winebroadcasttrueglobalall +wss://eden.nostr.landv +wss://me.dm +wss://emacs.ch +wss://winonostr.wine +wss://infoplebstr.com +wss://mas.towss +wss://gruene.social +wss://relay.freeplace.nl +wss://itis.to +wss://bsky.social +wss://lnbits.thefockinfury.wtf +wss://5xxkt7zvmh4zdsjw64lgvchjdlrrgw4w2huujiiud35qms6gnkn5azad.onion +wss://eientei.org +wss://artisan.chat +wss://nustr.mom +wss://relay.nostrhraph.net +wss://shitposter.club +wss://nostril.cam +wss://nostr.spaceshell.xyz +wss://relay.wtr.app +wss://tdd.social +wss://d3meec25b53kegrnjmtmtyynikbkmuxf4jqgtk3sonjs6e62hpaezyqd.onion +wss://tkz.one +wss://freerelay.xyz +wss://nfdn.betanet.dotalgo.io +wss://mitra.social +wss://hablanews.io +wss://calle.wtf +wss://voskey.icalo.net +wss://nostr.sloyhy.win +wss://framapiaf.org +wss://nostrnodeofsven.com +wss://ciberlandia.pt +wss://gnusocial.net +wss://relay.s3x.socia +wss://paste.2nodez.com +wss://union.place +wss://bofh.socia +wss://sovbit.dev +wss://lnbits.btc-payserver.eu +wss://osna.social +wss://im-in.space +wss://junxingwang.org +wss://relay.nostr.wirednet.jpcheck +wss://libranet.de +wss://fedisnap.com +wss://woodpecker.social +wss://gensokyo.town +wss://social.imirhil.fr +wss://links.potsda.mn +wss://law-and-politics.online +wss://nostrich.bar +wss://tweesecake.social +wss://nostr.kisiel.net.pl +wss://relay.kisiel.net.pl +wss://calckey.social +wss://nostr.cercatrowa.me +wss://meganekeesu.tokyo +wss://lndiscs.duckdns.org +wss://omochi.xyz +wss://wue.social +wss://nostr.libreleaf.com +wss://rusnak.io +wss://rsslay-production.up.railway.app +wss://nostr.yuhr.org +wss://bod4ojj37fneith2setv3qjbii563wesbjqgdipdz4ag6voic2xk5iad.onion +wss://fediverse.blog +wss://pouet.chapril.org +wss://baq5ufl2rnczpalnoqabxwpjm3kvhzduwgvptxzx7yq37oqdbgf65syd.local +wss://baq5ufl2rnczpalnoqabxwpjm3kvhzduwgvptxzx7yq37oqdbgf65syd.onion +wss://cryptodon.lol +wss://nostr.debancariser.com +wss://mizunashi.hostdon.ne.jp +wss://relay.deezy.io +wss://bbq.snoot.com +wss://historians.social +wss://mi.mashiro.site +wss://mastodonpost.social +wss://nostrpub.welliorder.net +wss://social.cologne +wss://metapixl.com +wss://wandzeitung.xyz +wss://lightninhrelay.com +wss://techopolis.social +wss://lnbits.btcpins.com +wss://purplenostrich.com +wss://onewilshire.la +wss://sself.co +wss://anygemini13.blogs.sapo.pt +wss://federated.press +wss://metadata.nostr.com +wss://nostr.hodl.ar +wss://goreslut.xyz +wss://mastodon.com.tr +wss://climatejustice.global +wss://brotka.st +wss://sueden.social +wss://mstdn.fr +wss://abid.cc +wss://lnbits.fuckedbitcoin.com +wss://meow.social +wss://nostr.rehab +wss://mstdn.media +wss://nodeo1.nostress.cc +wss://nostrmassmux.com +wss://sauropods.win +wss://civilians.social +wss://pnw.zone +wss://zebeedee.cloud +wss://nostr.walletofsatishi.com +wss://aufovmqaxj5nhqmtorhgpogdjxefhkff25cbyyjt2sub3vwg6b6rplid.onion +wss://forfuture.social +wss://76f67qcwxsxpz7cfozlzunota2ejqznpldc5pnqtyq233hjpjzrmlfid.local +wss://toot.garden +wss://umbrel.tail9dfb.ts.net:4848 +wss://mastodon.chasem.dev +wss://misskey.pm +wss://merovingian.club +wss://chirp.enworld.org +wss://paid.no.str.ce +wss://masto.bike +wss://masto.1146.nohost.me +wss://eosla.comno-str.orgrelay.zeh.appno-str.orgrelay.zeh.app +wss://nixnet.social +wss://pay.zapit.live +wss://respublicae.eu +wss://nekomiya.net +wss://mastodon.internet-czas-dzialac.pl +wss://plushies.social +wss://lnb.openchain.fr +wss://mastodon.lol +wss://social.rebellion.global +wss://ruhr.social +wss://mi.farland.world +wss://pkutalk.com +wss://systemli.social +wss://nostr.minimue81.selfhost.com +wss://mastodon.la +wss://everything.happens.horse +wss://pagan.plus +wss://clacks.link +wss://u-tokyo.social +wss://fediverse.projectftm.com +wss://mastodon.bachgau.social +wss://social.kabi.tk +wss://ty3zdjkwlxo4zah6tgdoolznjcbvkhxpcvjyqe2buxeg23hbeyvr3rad.local +wss://pleroma.wakuwakup.net +wss://social.horrorhub.club +wss://nostr.nightowlstudios.ca +wss://mastodon.ml +wss://relay.orangepillapp.com +wss://misskey.sup39.dev +wss://mfmf.club +wss://pokemon.mastportal.info +wss://gohan-oisii.net +wss://aipi.social +wss://nostr.semisol.com +wss://oslo.town +wss://relay.layer.systems +wss://naharia.net +wss://social.elbespace.de +wss://linuxrocks.online +wss://b81m3pf94ridtry53g8ufyyrjtjaoxgbyjbs5k8qrqkr1whocxiy.loki:8080 +wss://lvl01.tater.ninja +wss://kinky.business +wss://relay.bitblockboom.com +wss://fedi.omada.cafe +wss://social.secret-wg.org +wss://celebrity.social +wss://weirdo.network +wss://mastodon.design +wss://berlin.social +wss://misskey.yukineko.me +wss://mindmachine.688.org +wss://sackheads.social +wss://a.farook.org +wss://social.ridetrans.it +wss://nostr-2.crypticthreadz.com +wss://test.itas.li +wss://fashionsocial.host +wss://ordinary.cafe +wss://social.arinbasu.online +wss://nostr.crypticthreadz.com +wss://relay.zebedee.cloud +wss://nostr.pub.wellorder.net +wss://09d4-5-161-189-144.ngrok-free.app +wss://andalucia.social +wss://udongein.xyz +wss://squeet.me +wss://mastodon.org.uk +wss://guild.pmdcollab.org +wss://relay.fi +wss://black.nostrscity.club +wss://relay.darker.to +wss://denostr.paiya.app +wss://noste.lu.ke +wss://wss.node01.nostress.cc +wss://theverge.space +wss://swiss.social +wss://relay.webstr.org +wss://nostr.shsbt.xyz +wss://relay.nostr.mom +wss://bitcoinmaximlaists.online +wss://relay.openhoofd.nl +wss://toot.aquilenet.fr +wss://toot.ale.gd +wss://relay.devstr.org +wss://lounge.town +wss://amala.schwartzwelt.xyz +wss://planetasieve.com.br +wss://alcrypt.ru:20911 +wss://webzero.grin.plus:8080 +wss://pylons.lightlns.com:28556 +wss://etourneau.fr:28343 +wss://sentie.relay.rts.network +wss://hermes.boarstudios.com +wss://non-central.pw +wss://nostrum.casa +wss://press.coop +wss://neovibe.app +wss://mstdn.starnix.network +wss://nostr.ameristraliagov.com +wss://cryptodon.chat +wss://umbrell.local:4848 +wss://mastodon.codingfield.com +wss://fe.disroot.org +wss://national.catposting.agency +wss://mastodon.pinewoodroad.net +wss://podcasts.social +wss://20nostr.semisol.dev +wss://nostr.oxtr.net +wss://mstdn.o-nature-culture.net +wss://dearcoati6.lnbits.com +wss://die-partei.social +wss://donotban.com +wss://creative.ai +wss://metaskey.net +wss://spacey.space +wss://node01.nostreess.cc +wss://node01.nostress.co +wss://niscii.xyz +wss://3gkpphcfwb6w5iq6axnmlbvr7pz2t37uy4ofocyijzttzrbz4jy43fid.onion +wss://3gkpphcfwb6w5iq6axnmlbvr7pz2t37uy4ofocyijzttzrbz4jy43fid.local +wss://sportsbots.xyz +wss://videos.lukesmith.xyz +wss://nostr.btcfreedom.ca +wss://gardenstate.social +wss://bg-btc.local:4848 +wss://0fa53e299287.ngrok.app +wss://bird.makeup +wss://nlayer.lbdev.fun +wss://relay.queiroz.vip +wss://mstdn.business +wss://osage.moe +wss://botrelay.com +wss://filter.wine +wss://gingadon.com +wss://noncentral.pw +wss://honi.club +wss://xn--baw-joa.social +wss://nostr.semisol.devwss +wss://relay.iris.to +wss://mynostrrelay.deno.dev +wss://social.heise.de +wss://vavursybkbgfyow7nnst5jnqsj2xyteusf3zeerbjdizq6y7h25v4syd.onion:5051 +wss://vavursybkbgfyow7nnst5jnqsj2xyteusf3zeerbjdizq6y7h25v4syd.onion:5050 +wss://relao.nostr.bg +wss://phpc.social +wss://mastodon.kylerank.in +wss://gameliberty.club +wss://rot.gives +wss://www.nostrweb.xyz +wss://ligma.pro +wss://mastodon.grin.hu +wss://geeknews.chat +wss://devdilettante.com +wss://relay.nosr-latam.link +wss://nosr.bitcoiner.social +wss://raru.re +wss://create-key.net +wss://lgbtqia.space +wss://fluffy.family +wss://mas.town +wss://bird.froth.zone +wss://akkoma.cryptoschizo.club +wss://relay.ramus.io +wss://40two.site +wss://relay.40two.site +wss://vis.social +wss://mk.paritybit.ca +wss://nyan.network +wss://ln.weedstr.net +wss://wikis.world +wss://social.fringe.com +wss://umbraxenu.no-ip.biz +wss://heads.social +wss://tsqdakwo4dh5ej3llsi52ftxfbialteu3jm4cmvxaksl3psbyeoyxxqd.onion +wss://jameliris.to +wss://mastodon.thirring.org +wss://miniwa.moe +wss://welcom.nostr.wine +wss://nostr.vulpem.comwss +wss://relay.semisol.dev +wss://kitsunes.club +wss://songbird.cloud +wss://nostr.wyssblitz.org +wss://libera.tokyo +wss://trpger.us +wss://comam.es +wss://nostr-pub.semisol.devaddittoyourrel +wss://social.dev-wiki.de +wss://ostfrie.se +wss://darmstadt.social +wss://nostr.cx.ms +wss://alentours.cc +wss://nostr.kleofash.eu +wss://gib.social +wss://test23.hifish.org +wss://rapemeat.solutions +wss://filter.stealth.winebroadcasttrue +wss://nostr.montre +wss://grumble.social +wss://nostr.roli.social +wss://primarycare.app +wss://hodlr.rocks +wss://superlinks.me +wss://mastodon.lawprofs.org +wss://tictoc.social +wss://nostest.dojotunnel.online +wss://kafka.icu +wss://nostr.lanparty.one +wss://filter.nostr.wineglobaltrue +wss://social.b10m.net +wss://worm.pink +wss://nostr-test.cx.ms +wss://nostrverifired.com +wss://relay.nostrss.re +wss://eliitin-some.fi +wss://dju.social +wss://jeremy.hu +wss://stream.criminallycute.fi +wss://nostr.0x50.dev +wss://n.s.nyc +wss://n.8.s.nyc +wss://relay.rocks +wss://relay.fiatjaf.com +wss://n-lan.s.nyc +wss://sciences.social +wss://nostr.swiss-enigma.com +wss://quietplace.xyz +wss://universe.nostrich.landlangenlangzh +wss://5280.city +wss://feddit.de +wss://base.lc +wss://social.medusmedia.com +wss://umha4zl6xk62a4dous6e7tq4qlmt462hlzs2su33en6qrvtvs3hkjgid.onion +wss://etorneau.fr:28343 +wss://foggyminds.com +wss://neurodiversity-in.au +wss://nostr.hendrixson.net +wss://ramen-fsm.eu.org +wss://www.nostrical.com +wss://feedbeat.me +wss://relay.bsky.social +wss://babka.social +wss://mastodontech.de +wss://commiespace.duckdns.org +wss://openbiblio.social +wss://karkatdyinginagluetrap.com +wss://relay.fundr.vanderwarker.family +wss://hannover.town +wss://nostr.relay.info +wss://mastodon.tetaneutral.net +wss://cache2.primal.net +wss://nostr.petrkr.net +wss://ifwo.eu +wss://mastodong.lol +wss://venera.social +wss://wallets.fyoumoneypod.com +wss://nostr.relay-nokotaro.com +wss://social.exozy.me +wss://gqgjp2bun4opme6mepz3rrgprkw4xatb6h5ogayorqwi6sajsxcp5sad.local +wss://branle.netlify.app +wss://rsslay.fiat.jaf +wss://chrislace.damus.io +wss://relay.leafbodhi.com +wss://social.opendesktop.org +wss://relay.nostr.watch +wss://gearlandia.haus +wss://freiburg.social +wss://atlas.nostro.land +wss://nostr.io +wss://patrizio.tn.al +wss://chaintools.io +wss://kn.icu +wss://relaywithme.eu +wss://la-autopilot-this-end-up.dvm.email +wss://coinfinity.co +wss://assemblag.es +wss://qou7zzll2mxx2ehl73n6pptmhizl5b3entowljlin3sqhcvltxdtlmad.onion +wss://indigenouscreatives.social +wss://bookwyrm.social +wss://kokoro.shugetsu.space +wss://eden.nost.land +wss://misskey.gothloli.club +wss://sersleepy.com +wss://rsslay.nos.pink +wss://pettingzoo.co +wss://witches.live +wss://7craxnzfi42touzi23etut5qjzqro27sqcuottxj7opntcin4fstruad.onion +wss://mastodonsweden.se +wss://mv2k.com +wss://nostr.tchaicap.space +wss://relay.whoop.ph +wss://bitcoiner.nostr.social +wss://kinkyelephant.com +wss://www.superstork.org +wss://mastodon.iftas.org +wss://lea.pet +wss://zug.network +wss://homeserver.local:4848 +wss://frontrange.co +wss://sciencemastodon.com +wss://montereybay.social +wss://social.securecryptomining.com +wss://rssrelay.nostr.moe +wss://nostr.org +wss://nostr.tw +wss://nostr.hk +wss://wxw.moe +wss://mastodonmusic.social +wss://puntarella.party +wss://oyasumi.space +wss://drumstodon.net +wss://social.wikimedia.de +wss://iyasaretai.pw +wss://social.coletivos.org +wss://nostream.localtest.me +wss://ubuntu201.local:4848 +wss://masto.pt +wss://taiwan.riley-tech.net +wss://freeradical.zone +wss://blastrf7z.xyz +wss://onemorestop.photo +wss://frikiverse.zone +wss://toot.bldrweb.org +wss://electroverse.tech +wss://mstdn.games +wss://relay1.nostr.unitedfop.com +wss://gratefuldread.masto.host +wss://mk.gabe.rocks +wss://widerweb.org +wss://mastodon.eternalaugust.com +wss://lay.southeastasia.cloudapp.azure.com:445 +wss://me.ns.ci +wss://gnostr.th +wss://fritter.cn +wss://nex.cn +wss://nex.tw +wss://fritter.jp +wss://fritter.tw +wss://gnostr.cn +wss://mstdn.dk +wss://akkoma.simulacrum-emporium.eu +wss://dalliance.social +wss://toot.re +wss://avatastic.uk +wss://blastr20f7z.xyz +wss://the.voiceover.bar +wss://porcodon.net +wss://nostr.dncn.xyz +wss://nostr.dnxn.xyz +wss://relay.xmr.rocks +wss://d6egak3woofrixu26gr3utb5qezhkktavsuwlrfqaauu55lmpudxudqd.onion:5051 +wss://social.wuebbsy.com +wss://4kgwkcfzea2xhefsquktyxqyjf3rsxa7oo7hxbcs6k3xdpxznknydsqd.local +wss://4kgwkcfzea2xhefsquktyxqyjf3rsxa7oo7hxbcs6k3xdpxznknydsqd.onion +wss://tooters.org +wss://nostre.wine +wss://relay.xplive.local +wss://relay2.xplive.local +wss://4v5umvicfs6a7d3aiy67uu2ibttiuanl2cehfmv5qaorbojousbgkdad.onion +wss://pipou.academy +wss://opjk6jxrcyicuwhe62tqy6zwx776u7rfi6cqo6iodurjvege7piz5wqd.local +wss://mythology.social +wss://nostr.millou.lol +wss://ryogrid.net:7777 +wss://nost.debancariser.com +wss://fault.stsecurity.moe +wss://jan-optiplex-5040.local:4848 +wss://relay-jp.wirednet.jp +wss://mastodon.kitchen +wss://relay.mnethome.de +wss://verkehrswende.social +wss://nostr.gleeze.com +wss://dair-community.social +wss://shota.house +wss://kavlak.uk +wss://social.inex.rocks +wss://4v5umvicfs6a7d3aiy67uu2ibttiuanl2cehfmv5qaorbojousbgkdad.local +wss://startrekshitposting.com +wss://nostrfmar.ddns.net +wss://4yqp7gzuf15zfc3hpwhz3j5p2uarvdsnf75ovpdiqvyjdsmku771jfid.onion +wss://nostrgraph.net +wss://idolheaven.org +wss://mstdn.kemono-friends.info +wss://mastodon.bawue.social +wss://social.pmj.rocks +wss://ursal.zone +wss://nstr.milou.lol +wss://poweredbygay.social +wss://gochisou.photo +wss://lnb3.openchain.fr +wss://nostr.filmweb.pl +wss://nostr-word.h3z.jp +wss://blastr.f7z.xyzanotherinstanceofblastr +wss://shakedown.social +wss://nostr.bubu.hair +wss://hyper-nostr.inosta.cc +wss://ieji.de +wss://wawmartme.com +wss://nostr.kungfu-g.rip +wss://bozgor.org +wss://todon.nl +wss://nostpy.lol +wss://ostatus.taiyolab.com +wss://polsum.rocks +wss://freespeech.group +wss://im.allmendenetz.de +wss://shitpost.poridge.club +wss://twingyeo.kr +wss://social.platypush.tech +wss://rsslay-production-bc22.up.railway.app +wss://lonely.damus.io +wss://kirche.social +wss://cubalibre.social +wss://relay.txinito.xyz +wss://realy.orangepill.dev +wss://3zi.ru +wss://plebstr.com +wss://social.seattle.wa.us +wss://social.bim.land +wss://cubhub.social +wss://relay.nostr-x.com +wss://hispagatos.space +wss://node101.nostress.cc +wss://lsbt.me +wss://jgqaglhautb4k6e6i2g34jakxiemqp6z4wynlirltuukgkft2xuglmqd.onion +wss://nostdemo.dojotunnel.online +wss://f.cz \ No newline at end of file diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/bloom/BloomFilter.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/bloom/BloomFilter.kt index 7da61abaf8..944d45e07b 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/bloom/BloomFilter.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/bloom/BloomFilter.kt @@ -49,13 +49,12 @@ class BloomFilter( fun add(value: HexKey) = add(value.hexToByteArray()) - fun print(): String { - val builder = StringBuilder() - for (seed in 0 until bits.size()) { - builder.append(if (bits.get(seed)) "1" else "0") + fun print() = + buildString { + for (seed in 0 until bits.size()) { + append(if (bits.get(seed)) "1" else "0") + } } - return builder.toString() - } fun add(value: ByteArray) { lock.write { 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 index a6ae257b0b..822da28530 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/bounties/BountyAddValueEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/bounties/BountyAddValueEvent.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.quartz.experimental.bounties -import com.vitorpamplona.quartz.nip01Core.EventHintBundle import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtag import com.vitorpamplona.quartz.nip01Core.tags.people.PTag 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 index 0baeff223d..621c9f852f 100644 --- 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 @@ -22,11 +22,11 @@ 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.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.core.any +import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.events.ETag import com.vitorpamplona.quartz.nip01Core.tags.events.eTag diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/EventHintBundle.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/EventHintBundle.kt similarity index 98% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/EventHintBundle.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/EventHintBundle.kt index 0f875e2df4..4f464f9e65 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/EventHintBundle.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/EventHintBundle.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.hints import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.Event diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/HintIndexer.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/HintIndexer.kt new file mode 100644 index 0000000000..b4b41e8b05 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/HintIndexer.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.nip01Core.hints + +import com.vitorpamplona.quartz.nip01Core.hints.bloom.BloomFilterMurMur3 + +/** + * Instead of having one bloom filter per relay, which could create many + * large filters for very few events, this class uses only one mega bloom + * filter and uses the hashcode of the relay uri as differentiator in salt. + */ +class HintIndexer( + size: Int = 10_000_000, + rounds: Int = 5, +) { + private val bloomFilter = BloomFilterMurMur3(size, rounds) + private val relayDB = hashSetOf() + + fun index( + id: ByteArray, + relay: String, + ) { + relayDB.add(relay) + bloomFilter.add(id, relay.hashCode()) + } + + fun get(id: ByteArray): List = + relayDB.filter { + bloomFilter.mightContain(id, it.hashCode()) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/HintProvider.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/HintProvider.kt new file mode 100644 index 0000000000..80e9521893 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/HintProvider.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.nip01Core.hints + +import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint +import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint +import com.vitorpamplona.quartz.nip01Core.hints.types.PubKeyHint + +interface HintProvider { + fun eventHints(): EventIdHint + + fun addressIdHints(): AddressHint + + fun pubKeyHints(): PubKeyHint +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/bloom/BitSetExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/bloom/BitSetExt.kt new file mode 100644 index 0000000000..be4ef3b55c --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/bloom/BitSetExt.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.hints.bloom + +import java.util.BitSet + +fun BitSet.printBits() = + buildString { + for (seed in 0 until size()) { + append(if (this@printBits.get(seed)) "1" else "0") + } + } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/bloom/BloomFilterMurMur3.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/bloom/BloomFilterMurMur3.kt new file mode 100644 index 0000000000..e4e96b320a --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/bloom/BloomFilterMurMur3.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.nip01Core.hints.bloom + +import com.vitorpamplona.quartz.utils.RandomInstance +import java.util.Base64 +import java.util.BitSet +import java.util.concurrent.locks.ReentrantReadWriteLock +import kotlin.concurrent.read +import kotlin.concurrent.write + +class BloomFilterMurMur3( + private val size: Int, + private val rounds: Int, + private val bits: BitSet = BitSet(size), + private val commonSalt: Int = RandomInstance.int(), +) { + private val hasher = MurmurHash3() + private val lock = ReentrantReadWriteLock() + + fun add( + value: ByteArray, + salt: Int = commonSalt, + ) { + lock.write { + repeat(rounds) { + bits.set(hash(value, salt + it)) + } + } + } + + fun mightContain( + value: ByteArray, + salt: Int = commonSalt, + ): Boolean { + lock.read { + repeat(rounds) { + if (!bits.get(hash(value, salt + it))) return false + } + return true + } + } + + private fun hash( + value: ByteArray, + seed: Int, + ) = hasher.hash(value, seed).mod(size) + + fun encode() = encode(this) + + fun printBits() = bits.printBits() + + companion object { + fun encode(f: BloomFilterMurMur3): String { + val bitSetB64 = Base64.getEncoder().encodeToString(f.bits.toByteArray()) + return "${f.size}:${f.rounds}:$bitSetB64:${f.commonSalt}" + } + + fun decode(encodedStr: String): BloomFilterMurMur3 { + val (sizeStr, roundsStr, filterB64, salt) = encodedStr.split(":") + val bitSet = BitSet.valueOf(Base64.getDecoder().decode(filterB64)) + return BloomFilterMurMur3(sizeStr.toInt(), roundsStr.toInt(), bitSet, salt.toInt()) + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/bloom/MurmurHash3.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/bloom/MurmurHash3.kt new file mode 100644 index 0000000000..85c82db3f4 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/bloom/MurmurHash3.kt @@ -0,0 +1,121 @@ +/** + * 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.hints.bloom + +class MurmurHash3 { + fun hash( + data: ByteArray, + seed: Int, + ) = hash(data, 0, data.size, seed) + + /** + * Generates 32 bit hash . + * @param data the byte array to hash + * @param offset the start offset of the data in the array (always 0) + * @param length the length of the data in the array + * @param seed the seed for the hash (int) + * @return 32 bit hash of the given array + */ + fun hash( + data: ByteArray, + offset: Int, + length: Int, + seed: Int, + ): Int { + val c1 = -0x3361d2af // 0xcc9e2d51 + val c2 = 0x1b873593 + var h1 = seed + val roundedEnd = offset + (length and 0xFFFFFFFC.toInt()) // Round down to 4-byte blocks + + var i = offset + while (i < roundedEnd) { + var k1 = + (data[i].toInt() and 0xFF) or + ((data[i + 1].toInt() and 0xFF) shl 8) or + ((data[i + 2].toInt() and 0xFF) shl 16) or + ((data[i + 3].toInt() and 0xFF) shl 24) + + i += 4 + + k1 *= c1 + k1 = Integer.rotateLeft(k1, 15) + k1 *= c2 + + h1 = h1 xor k1 + h1 = Integer.rotateLeft(h1, 13) + h1 = h1 * 5 + -0x19ab949c // 0xe6546b64 + } + + // processing tail (remaining bytes) + var k1 = 0 + when (length and 3) { + 3 -> { + k1 = k1 or ((data[i + 2].toInt() and 0xFF) shl 16) + k1 = k1 or ((data[i + 1].toInt() and 0xFF) shl 8) + k1 = k1 or (data[i].toInt() and 0xFF) + + k1 *= c1 + k1 = Integer.rotateLeft(k1, 15) + k1 *= c2 + + h1 = h1 xor k1 + } + + 2 -> { + k1 = k1 or (data[i + 1].toInt() and 0xFF shl 8) + k1 = k1 or (data[i].toInt() and 0xFF) + + k1 *= c1 + k1 = Integer.rotateLeft(k1, 15) + k1 *= c2 + + h1 = h1 xor k1 + } + + 1 -> { + k1 = k1 or (data[i].toInt() and 0xFF) + + k1 *= c1 + k1 = Integer.rotateLeft(k1, 15) + k1 *= c2 + + h1 = h1 xor k1 + } + } + + // final mix + h1 = h1 xor length + h1 = fmix32(h1) + + return h1 + } + + private fun fmix32(h: Int): Int { + var f = h + f = f xor (f ushr 16) + f *= -0x7a143595 // 0x85ebca6b + f = f xor (f ushr 13) + f *= -0x3d4d51cb // 0xc2b2ae35 + f = f xor (f ushr 16) + + return f + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/types/AddressHint.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/types/AddressHint.kt new file mode 100644 index 0000000000..e83191bc07 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/types/AddressHint.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.nip01Core.hints.types + +class AddressHint( + val addressId: String, + var relay: String? = null, +) : Hint { + override fun id() = addressId.toByteArray(Charsets.UTF_8) +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/types/EventIdHint.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/types/EventIdHint.kt new file mode 100644 index 0000000000..80faa2149d --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/types/EventIdHint.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.hints.types + +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray + +class EventIdHint( + val eventId: HexKey, + var relay: String? = null, +) : Hint { + override fun id() = eventId.hexToByteArray() +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/types/Hint.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/types/Hint.kt new file mode 100644 index 0000000000..591a3e6858 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/types/Hint.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.nip01Core.hints.types + +interface Hint { + fun id(): ByteArray +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/types/PubKeyHint.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/types/PubKeyHint.kt new file mode 100644 index 0000000000..431c121215 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/types/PubKeyHint.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.hints.types + +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray + +class PubKeyHint( + val pubkey: HexKey, + var relay: String? = null, +) : Hint { + override fun id() = pubkey.hexToByteArray() +} 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 ab0fc0594b..f11057a247 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/OtsEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/OtsEvent.kt @@ -21,11 +21,11 @@ package com.vitorpamplona.quartz.nip03Timestamp import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.EventHintBundle import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray +import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.events.ETag import com.vitorpamplona.quartz.nip01Core.tags.events.eTag diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/messages/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/messages/TagArrayBuilderExt.kt index 2b6e87cdaf..b4abcce1a0 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/messages/TagArrayBuilderExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/messages/TagArrayBuilderExt.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.quartz.nip04Dm.messages -import com.vitorpamplona.quartz.nip01Core.EventHintBundle import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip01Core.tags.events.EventReference import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag 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 ac2f361693..1c9e743dd5 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/TextNoteEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/TextNoteEvent.kt @@ -21,9 +21,9 @@ package com.vitorpamplona.quartz.nip10Notes import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.EventHintBundle import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip10Notes.tags.markedETags import com.vitorpamplona.quartz.nip10Notes.tags.prepareETagsAsReplyTo 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 index 95f653af69..62da19c06f 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/tags/ReplyBuilder.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/tags/ReplyBuilder.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.quartz.nip10Notes.tags -import com.vitorpamplona.quartz.nip01Core.EventHintBundle +import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent 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 d4f6dc232a..0ad78c9ea6 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/NIP17Factory.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/NIP17Factory.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.quartz.nip17Dm -import com.vitorpamplona.quartz.nip01Core.EventHintBundle import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent 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 index ea04eefcac..0d2e616232 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/files/ChatMessageEncryptedFileHeaderEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/files/ChatMessageEncryptedFileHeaderEvent.kt @@ -21,9 +21,9 @@ package com.vitorpamplona.quartz.nip17Dm.files import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.EventHintBundle import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.events.ETag import com.vitorpamplona.quartz.nip01Core.tags.people.PTag 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 index e875d8ab4f..36d4001f0b 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/files/TagArrayBuilderExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/files/TagArrayBuilderExt.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.quartz.nip17Dm.files -import com.vitorpamplona.quartz.nip01Core.EventHintBundle import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip01Core.tags.people.pTag import com.vitorpamplona.quartz.nip01Core.tags.people.pTags diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/messages/ChatMessageEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/messages/ChatMessageEvent.kt index a6802b3646..8679363ea4 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/messages/ChatMessageEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/messages/ChatMessageEvent.kt @@ -21,9 +21,9 @@ package com.vitorpamplona.quartz.nip17Dm.messages import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.EventHintBundle import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.events.ETag import com.vitorpamplona.quartz.nip01Core.tags.people.PTag 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 index c81a70808c..84c0059ae7 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/messages/TagArrayBuilderExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/messages/TagArrayBuilderExt.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.quartz.nip17Dm.messages -import com.vitorpamplona.quartz.nip01Core.EventHintBundle import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip01Core.tags.people.pTag import com.vitorpamplona.quartz.nip01Core.tags.people.pTags 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 25909ec22f..1b430201b5 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip21UriScheme/EventExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip21UriScheme/EventExt.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.quartz.nip21UriScheme -import com.vitorpamplona.quartz.nip01Core.EventHintBundle import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip19Bech32.toNIP19 fun Event.toNostrUri(): String = "nostr:${toNIP19()}" 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 f35d88fbae..c65fd1a34f 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/CommentEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/CommentEvent.kt @@ -21,11 +21,11 @@ package com.vitorpamplona.quartz.nip22Comments import androidx.compose.runtime.Immutable -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.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent import com.vitorpamplona.quartz.nip21UriScheme.toNostrUri 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 0cc88e96a0..9f59203549 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip25Reactions/ReactionEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip25Reactions/ReactionEvent.kt @@ -21,10 +21,10 @@ package com.vitorpamplona.quartz.nip25Reactions import androidx.compose.runtime.Immutable -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.HexKey +import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.addressables.aTag import com.vitorpamplona.quartz.nip01Core.tags.events.ETag diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelHideMessageEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelHideMessageEvent.kt index 69827dab85..612c2625ad 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelHideMessageEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelHideMessageEvent.kt @@ -21,9 +21,9 @@ package com.vitorpamplona.quartz.nip28PublicChat.admin import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.EventHintBundle import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.events.ETag import com.vitorpamplona.quartz.nip01Core.tags.events.eTags 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 index e37e0ce5c8..2caac83ed9 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelMetadataEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelMetadataEvent.kt @@ -21,9 +21,9 @@ package com.vitorpamplona.quartz.nip28PublicChat.admin import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.EventHintBundle import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.events.ETag import com.vitorpamplona.quartz.nip28PublicChat.base.BasePublicChatEvent diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelMuteUserEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelMuteUserEvent.kt index 3f97d28f73..101c848adc 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelMuteUserEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelMuteUserEvent.kt @@ -21,9 +21,9 @@ package com.vitorpamplona.quartz.nip28PublicChat.admin import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.EventHintBundle import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip01Core.tags.people.pTags 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 index 71a5e61e19..cc8b85be99 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/base/TagArrayBuilderExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/base/TagArrayBuilderExt.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.quartz.nip28PublicChat.base -import com.vitorpamplona.quartz.nip01Core.EventHintBundle import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip01Core.tags.events.ETag import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip01Core.tags.people.pTag 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 index f5947024d3..4c5c418e4c 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/message/ChannelMessageEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/message/ChannelMessageEvent.kt @@ -21,9 +21,9 @@ package com.vitorpamplona.quartz.nip28PublicChat.message import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.EventHintBundle import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.events.ETag import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent 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 index 7bfe405353..80cbb9139d 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip30CustomEmoji/selection/EmojiPackSelectionEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip30CustomEmoji/selection/EmojiPackSelectionEvent.kt @@ -21,10 +21,10 @@ package com.vitorpamplona.quartz.nip30CustomEmoji.selection import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.EventHintBundle import com.vitorpamplona.quartz.nip01Core.core.BaseReplaceableEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.signers.eventUpdate import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/issue/GitIssueEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/issue/GitIssueEvent.kt index e17c49ecaa..bf59e87107 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/issue/GitIssueEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/issue/GitIssueEvent.kt @@ -21,9 +21,9 @@ package com.vitorpamplona.quartz.nip34Git.issue import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.EventHintBundle import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags 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 index 7a99b82fda..1f81b3f5d9 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/issue/TagArrayBuilderExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/issue/TagArrayBuilderExt.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.quartz.nip34Git.issue -import com.vitorpamplona.quartz.nip01Core.EventHintBundle import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip01Core.tags.people.pTags 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 index e65622051a..f7dd714120 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/reply/GitReplyEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/reply/GitReplyEvent.kt @@ -21,9 +21,9 @@ package com.vitorpamplona.quartz.nip34Git.reply import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.EventHintBundle import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent 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 index a3634c4105..4965013299 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/reply/TagArrayBuilderExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/reply/TagArrayBuilderExt.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.quartz.nip34Git.reply -import com.vitorpamplona.quartz.nip01Core.EventHintBundle import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip01Core.tags.people.pTags 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 12f83a3895..9389eab686 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip35Torrents/TorrentCommentEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip35Torrents/TorrentCommentEvent.kt @@ -21,9 +21,9 @@ package com.vitorpamplona.quartz.nip35Torrents import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.EventHintBundle import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.events.ETag 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 index 6162821532..8eba6cb104 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/chat/LiveActivitiesChatMessageEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/chat/LiveActivitiesChatMessageEvent.kt @@ -21,9 +21,9 @@ package com.vitorpamplona.quartz.nip53LiveActivities.chat import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.EventHintBundle import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent 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 index dc1ea675c3..07302df2c0 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/chat/TagArrayBuilderExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/chat/TagArrayBuilderExt.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.quartz.nip53LiveActivities.chat -import com.vitorpamplona.quartz.nip01Core.EventHintBundle import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip01Core.tags.people.pTag diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/approval/CommunityPostApprovalEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/approval/CommunityPostApprovalEvent.kt index c888bc4e83..718b0c5616 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/approval/CommunityPostApprovalEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/approval/CommunityPostApprovalEvent.kt @@ -22,10 +22,10 @@ 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.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedATags import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedAddresses 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 index 7b8e26e1d4..d003f195b4 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/approval/TagArrayBuilderExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/approval/TagArrayBuilderExt.kt @@ -20,10 +20,10 @@ */ 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.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent fun TagArrayBuilder.community(event: EventHintBundle) = add(event.toATag().toATagArray()) 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 cfbf375ef2..e50abb413d 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,12 @@ package com.vitorpamplona.quartz.nip75ZapGoals import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.EventHintBundle 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.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags import com.vitorpamplona.quartz.nip01Core.tags.references.reference diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/utils/RandomInstance.kt b/quartz/src/main/java/com/vitorpamplona/quartz/utils/RandomInstance.kt index 90b0c95f3b..01b03b8217 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/utils/RandomInstance.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/utils/RandomInstance.kt @@ -25,7 +25,7 @@ import java.security.SecureRandom object RandomInstance { private val randomizer = SecureRandom() - fun int(bound: Int) = randomizer.nextInt(bound) + fun int(bound: Int = Int.MAX_VALUE) = randomizer.nextInt(bound) fun bytes(size: Int) = ByteArray(size).also { randomizer.nextBytes(it) } } diff --git a/quartz/src/test/java/com/vitorpamplona/quartz/nip01Core/hints/BloomFilterMurMur3Test.kt b/quartz/src/test/java/com/vitorpamplona/quartz/nip01Core/hints/BloomFilterMurMur3Test.kt new file mode 100644 index 0000000000..17d99b277b --- /dev/null +++ b/quartz/src/test/java/com/vitorpamplona/quartz/nip01Core/hints/BloomFilterMurMur3Test.kt @@ -0,0 +1,114 @@ +/** + * 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.hints + +import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray +import com.vitorpamplona.quartz.nip01Core.hints.bloom.BloomFilterMurMur3 +import com.vitorpamplona.quartz.utils.RandomInstance +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertFalse +import junit.framework.TestCase.assertTrue +import org.junit.Test + +class BloomFilterMurMur3Test { + val testEncoded = "100:10:AKiEIEQKALgRACEABA==:3" + val testInBinary = "00000000000101010010000100000100001000100101000000000000000111011000100000000000100001000000000000100000000000000000000000000000" + + val key1 = "ca29c211f1c72d5b6622268ff43d2288ea2b2cb5b9aa196ff9f1704fc914b71b".hexToByteArray() + val key2 = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c".hexToByteArray() + val key3 = "560c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c".hexToByteArray() + + val keys = + mutableListOf().apply { + for (seed in 0..1_000_000) { + add(RandomInstance.bytes(32)) + } + } + + @Test + fun testCreate() { + val bloomFilter = BloomFilterMurMur3(100, 10, commonSalt = 3) + bloomFilter.add(key1) + bloomFilter.add(key2) + + assertEquals(testEncoded, bloomFilter.encode()) + assertEquals(testInBinary, bloomFilter.printBits()) + + assertTrue(bloomFilter.mightContain(key1)) + assertTrue(bloomFilter.mightContain(key2)) + + assertFalse(bloomFilter.mightContain(key3)) + } + + @Test + fun testDecoding() { + val bloomFilter = BloomFilterMurMur3.decode(testEncoded) + + assertEquals(testEncoded, bloomFilter.encode()) + assertEquals(testInBinary, bloomFilter.printBits()) + + assertTrue(bloomFilter.mightContain(key1)) + assertTrue(bloomFilter.mightContain(key2)) + + assertFalse(bloomFilter.mightContain(key3)) + } + + @Test + fun runProb() { + val bloomFilter = BloomFilterMurMur3.decode(testEncoded) + + var failureCounter = 0 + repeat(1_000_000) { + if (bloomFilter.mightContain(RandomInstance.bytes(32))) { + failureCounter++ + } + } + assertEquals(0, failureCounter) + } + + @Test + fun runProb2() { + val bloomFilter = BloomFilterMurMur3(10_000_000, 4) + keys.forEach(bloomFilter::add) + + var failureCounter = 0 + repeat(1_000_000) { + if (bloomFilter.mightContain(RandomInstance.bytes(32))) { + failureCounter++ + } + } + assertTrue("Failures $failureCounter ${failureCounter / 1_000_000.0}", failureCounter / 1_000_000.0f < 0.015) + } + + @Test + fun runProb3() { + val bloomFilter = BloomFilterMurMur3(10_000_000, 4) + keys.forEach(bloomFilter::add) + + var failureCounter = 0 + repeat(1_000_000) { + if (bloomFilter.mightContain(RandomInstance.bytes(32))) { + failureCounter++ + } + } + assertTrue("Failures $failureCounter ${failureCounter / 1_000_000.0}", failureCounter / 1_000_000.0f < 0.015) + } +} diff --git a/quartz/src/test/java/com/vitorpamplona/quartz/nip01Core/hints/HintIndexerTest.kt b/quartz/src/test/java/com/vitorpamplona/quartz/nip01Core/hints/HintIndexerTest.kt new file mode 100644 index 0000000000..7bff250128 --- /dev/null +++ b/quartz/src/test/java/com/vitorpamplona/quartz/nip01Core/hints/HintIndexerTest.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.nip01Core.hints + +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray +import com.vitorpamplona.quartz.nip01Core.core.toHexKey +import com.vitorpamplona.quartz.nip01Core.hints.types.PubKeyHint +import com.vitorpamplona.quartz.utils.RandomInstance +import junit.framework.TestCase.assertTrue +import org.junit.Test + +class HintIndexerTest { + val key1 = "ca29c211f1c72d5b6622268ff43d2288ea2b2cb5b9aa196ff9f1704fc914b71b".hexToByteArray() + val key2 = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c".hexToByteArray() + val key3 = "560c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c".hexToByteArray() + + val keys = + mutableListOf().apply { + for (seed in 0..1_000_000) { + add(RandomInstance.bytes(32).toHexKey()) + } + } + + val relays = + this.javaClass + .getResourceAsStream("relayDB.txt") + ?.readAllBytes() + .toString() + .split("\n") + + val keyHints = + keys + .map { key -> + (0..5).map { PubKeyHint(key, relays.random()) } + }.flatten() + + val key1Relays = (0..5).map { relays.random() } + val key2Relays = (0..4).map { relays.random() } + val key3Relays = (0..3).map { relays.random() } + + @Test + fun runExistingKeys() { + val indexer = HintIndexer() + keyHints.forEach { + indexer.index(it.id(), it.relay!!) + } + + var failureCounter = 0 + repeat(1000) { + val key = keys.random() + if (indexer.get(key.hexToByteArray()).isEmpty()) { + failureCounter++ + } + } + + assertTrue("Failures $failureCounter ${failureCounter / 1_000.0}", failureCounter / 1_000.0f < 0.015) + } + + @Test + fun runProb() { + val indexer = HintIndexer() + keyHints.forEach { + indexer.index(it.id(), it.relay!!) + } + + var failureCounter = 0 + repeat(1_000) { + if (indexer.get(RandomInstance.bytes(32)).isNotEmpty()) { + failureCounter++ + } + } + assertTrue("Failures $failureCounter ${failureCounter / 1_000.0}", failureCounter / 1_000.0f < 0.015) + } +} diff --git a/quartz/src/test/java/com/vitorpamplona/quartz/nip01Core/hints/relayDB.txt b/quartz/src/test/java/com/vitorpamplona/quartz/nip01Core/hints/relayDB.txt new file mode 100644 index 0000000000..0977f44b3a --- /dev/null +++ b/quartz/src/test/java/com/vitorpamplona/quartz/nip01Core/hints/relayDB.txt @@ -0,0 +1,2539 @@ +wss://nostr.wine +wss://relay.orangepill.dev +wss://xmr.usenostr.org +wss://nostr.portemonero.com +wss://nostr.xmr.rocks +wss://relay.nostr.band +wss://filter.nostr.wine +wss://nostr.milou.lol +wss://nostr.mutinywallet.com +wss://nostr-pub.wellorder.net +wss://nostr.zebedee.cloud +wss://nos.lol +wss://brb.io +wss://bitcoiner.social +wss://nostr.decentony.com +wss://relay.nostriches.org +wss://paid.spore.ws +wss://eden.nostr.land +wss://puravida.nostr.land +wss://5dzvuefllevkhk7miqynaviguedxfnofrayu2xwfwtlkdg4radjdlyqd.onion +wss://relay-jp.nostr.wirednet.jp +wss://relay.nostrich.land +wss://nostr.holybea.com +wss://nostr-relay.nokotaro.com +wss://nostr-paid.h3z.jp +wss://nostrja-kari.heguro.com +wss://nostr.mom +wss://nostr.fediverse.jp +wss://nostr.h3z.jp +wss://universe.nostrich.land +wss://nostr.uselessshit.co +wss://atlas.nostr.land +wss://relay.snort.social +wss://universe.nostrich.landlangenlanges +wss://nostr.slothy.win +wss://nostr.plebchain.org +wss://nostr-relay.untethr.me +wss://relay.nostr.com.au +wss://nostr.inosta.cc +wss://relay.nostrati.com +wss://nostr.bitcoiner.social +wss://relay.nostrplebs.com +wss://relay.nostr.info +wss://nostr-relay.wlvs.space +wss://nostr.oxtr.dev +wss://nostr.onsats.org +wss://relay.wellorder.net +wss://relay.plebstr.com +wss://no.str.cr +wss://nostr.walletofsatoshi.com +wss://nostr.mwmdev.com +wss://relay.nostr.bg +wss://nostr.rocks +wss://nostr.fmt.wiz.biz +wss://nostr.orangepill.dev +wss://nostr-pub.semisol.dev +wss://nostr.sandwich.farm +wss://relay.nostr.ch +wss://relay.orange-crush.com +wss://private.red.gb.net +wss://nostr.lnprivate.network +wss://nostr.lu.ke +wss://relay.nostr.wirednet.jp +wss://lightningrelay.com +wss://relay.nostrgraph.net +wss://relay.nostrica.com +wss://relay.mostr.pub +wss://nostr-sg.com +wss://nostr.zkid.social +wss://relay.nostr.vet +wss://relay.nostr3.io +wss://relay.arsip.my.id +wss://relay.current.fyi +wss://global.relay.red +wss://nostr.island.network +wss://node01.nostress.cc +wss://relay.nostr.net.in +wss://relay.utxo.one +wss://relay-1.arsip.my.id +wss://nostrical.com +wss://nostro.cc +wss://rsslay.nostr.moe +wss://relay.nostr.or.jp +wss://nostream.unift.xyz +wss://blastr.f7z.xyz +wss://relay.honk.pw +wss://universe.nostrich.landlangja +wss://nostream.ocha.one +wss://paid-relay.nost.love +wss://offchain.pub +wss://nostr-usa.ka1gbeoa21bnm.us-west-2.cs.amazonlightsail.com +wss://nostr.terminus.money +wss://nostr.shawnyeager.net +wss://public.nostr.swissrouting.com +wss://nostrex.fly.dev +wss://relay.727whisky.com +wss://relay.cryptocculture.com +wss://relay.bleskop.com +wss://nostr.lorentz.is +wss://nostr.actn.io +wss://nostr-relay.lnmarkets.com +wss://nostr.openchain.fr +wss://relay.punkhub.me +wss://nostr.blipme.app +wss://nostr.swiss-enigma.ch +wss://nostr-verified.wellorder.net +wss://at.nostrworks.com +wss://21sats.net +wss://e.nos.lol +wss://nostr.mikedilger.com +wss://nostr.azte.co +wss://noster.bitcoiner.social +wss://relay.nostr.moe +wss://nostream.nostrly.io +wss://nostr.gives.africa +wss://nostr.21sats.net +wss://bitcoinmaximalists.online +wss://paid.nostrified.org +wss://nostr.1sat.org +wss://nostr.relayer.se +wss://sg.qemura.xyz +wss://nostr.coinos.io +wss://nostr.bitcoinplebs.de +wss://nostrich.friendship.tw +wss://nostr.sg +wss://eosla.com +wss://nostr.sidnlabs.nl +wss://nostr.foundrydigital.com +wss://relay.nostrview.com +wss://nostr.semisol.dev +wss://relay.f7z.io +wss://wlvs.space +wss://nostr.v0l.io +wss://nostr-relay.digitalmob.ro +wss://rsslay.fiatjaf.com +wss://relay.theorangepillapp.com +wss://relay.zeh.app +wss://nostr.zoomout.chat +wss://relay.stoner.com +wss://nostr.cercatrova.me +wss://relay.ryzizub.com +wss://nostr-1.nbo.angani.co +wss://nostr21.com +wss://spore.ws +wss://nostrue.com +wss://no-str.org +wss://relay.taxi +wss://ragnar-relay.com +wss://relay.austrich.net +wss://relay.nostr-latam.link +wss://1.noztr.com +wss://relay.nostr.scot +wss://test.relay.nostrich.day +wss://jiggytom.ddns.net +wss://nostr.bongbong.com +wss://relay.nostromo.social +wss://relay.sendstr.com +wss://nostr-dev.universalname.space +wss://relay.nostrcheck.me +wss://nostr.libertasprimordium.com +wss://nostr.kollider.xyz +wss://expensive-relay.fiatjaf.com +wss://nostr-sandbox.minds.io +wss://relay.nostrich.de +wss://nostr.gromeul.eu +wss://relay.nostr.wine +wss://nostr.screaminglife.io +wss://nostr-relay.derekross.me +wss://nostrica.nostrnotes.com +wss://paid.no.str.cr +wss://nostr.sethforprivacy.com +wss://nostr.dumpit.top +wss://nostr.herci.one +wss://cheery-paddock-rsakdrtc35c55n6yregn.wnext.app +wss://nostr.blockpower.capital +wss://nostr.nym.life +wss://nostr-verif.slothy.win +wss://fiatdenier.com +wss://nostr.bitcoin-21.org +wss://nostr.fluidtrack.in +wss://nostr.developer.li +wss://r.ayit.org +wss://relay.nostr.nu +wss://nostr.bostonbtc.com +wss://rly.social +wss://nostr.bridgey.dev +wss://relay.nostrprotocol.net +wss://nostr.mado.io +wss://nostr.einundzwanzig.space +wss://nostr2.actn.io +wss://nostr-relay.freedomnode.com +wss://nostr.pleb.network +wss://nostr.mouton.dev +wss://eelay.current.fyi +wss://nostr.notmyhostna.me +wss://nostr.pjv.me +wss://nostr.jatm.link +wss://nostr.fractalized.ovh +wss://nostr-relay.app.ikeji.ma +wss://relayer.ocha.one +wss://nostr.com.de +wss://nostr-2.afarazit.eu +wss://nostr.l00p.org +wss://nostr.drss.io +wss://relay.nostrify.io +wss://nostr.radixrat.com +wss://nostr-relay.bitcoin.ninja +wss://nostrsatva.net +wss://nostr.mustardnodes.com +wss://nostr01.vida.dev +wss://nostr.noones.com +wss://nostr.easify.de +wss://nostr3.actn.io +wss://moonbreeze.richardbondi.net +wss://nostr.naut.social +wss://private-nostr.v0l.io +wss://nostr.zaprite.io +wss://nostr.lightninglinks.xyz +wss://nostr.hackerman.pro +wss://nr.yay.so +wss://nostr.roundrockbitcoiners.com +wss://nostr.sovbit.host +wss://nostrelay.yeghro.site +wss://pow32.nostr.land +wss://nostr.1729.cloud +wss://nostr.rdfriedl.com +wss://nostr.h4x0r.host +wss://nostr.up.railway.app +wss://nostr.lnorb.com +wss://nostr.lordkno.ws +wss://relay.nostr.vision +wss://nostr-3.orba.ca +wss://satstacker.cloud +wss://freedom-relay.herokuapp.com +wss://nostr-relay.freeberty.net +wss://nostr.unknown.place +wss://nostr.delo.software +wss://relay.nostr.pro +wss://relay.minds.com +wss://nostr.ono.re +wss://relay.grunch.dev +wss://relay.cynsar.foundation +wss://relay.oldcity-bitcoiners.info +wss://relay.bitid.nz +wss://relay.nostr.xyz +wss://relay.futohq.com +wss://relay.farscapian.com +wss://astral.ninja +wss://relay.sovereign-stack.org +wss://nostr-2.zebedee.cloud +wss://nostr.nymsrelay.com +wss://relay.kronkltd.net +wss://relay.r3d.red +wss://universe.nostrich.landlangen +wss://nostr-dev.wellorder.net +wss://nostr.beta3.dev +wss://nostr.data.haus +wss://nostr.hugo.md +wss://relay-dev.cowdle.gg +wss://relay.dwadziesciajeden.pl +wss://tmp-relay.cesc.trade +wss://nostr.massmux.com +wss://relay.nostr.africa +wss://nostr1.tunnelsats.com +wss://nostr.f44.dev +wss://relay.n057r.club +wss://nostr-verif.slothy.com +wss://nostr.1f52b.xyz +wss://nostr.sebastix.dev +wss://nostr.lightning.contact +wss://nostr.rly.social +wss://noster.online +wss://relay.lexingtonbitcoin.org +wss://nostr.bitcoinbay.engineering +wss://nostr.howtobitcoin.shop +wss://blg.nostr.sx +wss://deschooling.us +wss://foolay.nostr.moe +wss://freespeech.casa +wss://nostr-01.bolt.observer +wss://nostr-01.dorafactory.org +wss://nostr-au.coinfundit.com +wss://nostr-eu.coinfundit.com +wss://nostr-relay.alekberg.net +wss://nostr-pub1.southflorida.ninja +wss://nostr-relay.gkbrk.com +wss://nostr-relay.pcdkd.fyi +wss://nostr-relay.schnitzel.world +wss://nostr-us.coinfundit.com +wss://nostr.21crypto.ch +wss://nostr.600.wtf +wss://nostr.8e23.net +wss://nostr.app.runonflux.io +wss://nostr.arguflow.gg +wss://nostr.bch.ninja +wss://nostr.chainofimmortals.net +wss://nostr.cizmar.net +wss://nostr.cheeserobot.org +wss://nostr.coollamer.com +wss://nostr.corebreach.com +wss://nostr.cro.social +wss://nostr.easydns.ca +wss://nostr.globals.fans +wss://nostr.handyjunky.com +wss://nostr.itas.li +wss://nostr.sectiontwo.org +wss://nostr.spleenrider.one +wss://nostr.thibautrey.fr +wss://nostr.uthark.com +wss://nostr.vulpem.com +wss://nostr.w3ird.tech +wss://nostr.whoop.ph +wss://nostr.yuv.al +wss://nostr01.opencult.com +wss://nostre.cc +wss://nostream.denizenid.com +wss://nostring.deno.dev +wss://pow.nostrati.com +wss://relay-pub.deschooling.us +wss://nostr.jiashanlu.synology.me +wss://nostr.klabo.blog +wss://relay.valireum.net +wss://nostr.fly.dev +wss://nostr.nordlysln.net +wss://nostr.zerofeerouting.com +wss://rsslay.nostr.net +wss://nostr-relay.nonce.academy +wss://nostr.rewardsbunny.com +wss://lv01.tater.ninja +wss://nostr-2.orba.ca +wss://nostr.orba.ca +wss://nostr.supremestack.xyz +wss://nostrrelay.com +wss://relay.nostr.au +wss://nostr.oooxxx.ml +wss://nostr.yael.at +wss://nostr-relay.trustbtc.org +wss://nostr.namek.link +wss://nostr-relay.wolfandcrow.tech +wss://nostr.satsophone.tk +wss://relay.dev.kronkltd.net +wss://nostr2.namek.link +wss://relay.21spirits.io +wss://relay.minds.io +wss://nostr.d11n.net +wss://nostr.tunnelsats.com +wss://nostr.leximaster.com +wss://mule.platanito.org +wss://nostr.robotechy.com +wss://relay.nostrmoto.xyz +wss://relay.boring.surf +wss://nostr.gruntwerk.org +wss://nostr.hyperlingo.com +wss://nostr.ethtozero.fr +wss://nostr.nodeofsven.com +wss://nostr.jimc.me +wss://nostr.utxo.lol +wss://relay.nyx.ma +wss://nostr.shmueli.org +wss://wizards.wormrobot.org +wss://nostr.sovbit.com +wss://nostr.datamagik.com +wss://relay.nostrid.com +wss://nostr1.starbackr.me +wss://relay.nostr.express +wss://nostr.formigator.eu +wss://nostr.xpersona.net +wss://nostr.digitalreformation.info +wss://nostr-relay.usebitcoin.space +wss://nostr-alpha.gruntwerk.org +wss://nostr-relay.australiaeast.cloudapp.azure.com +wss://nostr-relay.smoove.net +wss://nostr-relay.j3s7m4n.com +wss://nostr.demovement.net +wss://nostr.thesimplekid.com +wss://nostr.aozing.com +wss://nostr.blocs.fr +wss://no.str.watch +wss://btc.klendazu.com +wss://nostr.mrbits.it +wss://nostr.zenon.wtf +wss://no.contry.xyz +wss://nostream.gromeul.eu +wss://relay.nostr.ro +wss://nostr.ncsa.illinois.edu +wss://nostr.itssilvestre.com +wss://nostr.chaker.net +wss://knostr.neutrine.com +wss://nostr.pobblelabs.org +wss://nostr.simatime.com +wss://relay.nosphr.com +wss://student.chadpolytechnic.com +wss://nostr.localhost.re +wss://nostr.coinsamba.com.br +wss://deconomy-netser.ddns.net:2121 +wss://nostr.21m.fr +wss://zur.nostr.sx +wss://nostr-relay.texashedge.xyz +wss://spleenrider.herokuapp.com +wss://nostr.bitcoin.sex +wss://relay.nostrzoo.com +wss://nostr.blockchaincaffe.it +wss://nostr-bg01.ciph.rs +wss://knostr.neutrine.com:8880 +wss://nostr.ahaspharos.de +wss://nostr.argdx.net +wss://nostr.snblago.com +wss://merrcurr.up.railway.app +wss://nostr.bingtech.tk +wss://relay.nostr.wf +wss://relay.koreus.social +wss://nostr.randomdevelopment.biz +wss://relay.nostr.hu +wss://relay.nostr.lu +wss://relay.nostr.ae +wss://middling.myddns.me:8080 +wss://nostr.nikolaj.online +wss://relay.nostrology.org +wss://nostr.satoshi.fun +wss://nostream.kinchie.snowinning.com +wss://nostr.lapalomilla.mx +wss://relay.thes.ai +wss://rsr.uyky.net:30443 +wss://nostrafrica.pcdkd.fyi +wss://nostr.bitcoin-basel.ch +wss://relay.21baiwan.com +wss://nostr.ddns.net:8008 +wss://free-relay.nostrich.land +wss://nostr.lukeacl.com +wss://nostr.ddns.net +wss://nostr.rocket-tech.net +wss://nostr-1.afarazit.eu +wss://nostr.0nyx.eu +wss://nostr-mv.ashiroid.com +wss://lbrygen.xyz +wss://nostr.community.networks.deavmi.assigned.network +wss://nostr.ownscale.org +wss://relay1.gems.xyz +wss://nostr.soscary.net +wss://nostr.0xtr.dev +wss://damus.io +wss://relay.alien.blue +wss://nostr.btcmp.com +wss://relayer.fiatjaf.com +wss://relay.lacosanostr.com +wss://adult.18plus.social +wss://nostrrr.bublina.eu.org +wss://relay.stoner +wss://nostr.pwnshop.cloud +wss://nostr.directory +wss://nostr-relay-dev.wlvs.space +wss://member.cash +wss://relay.nyc1.vinux.app +wss://nostr-relay.digitamob.ro +wss://nor.st +wss://nostr.topeth.info +wss://nostr.rocketstyle.com.au +wss://relay.tnano.duckdns.org +wss://nostr.21l.st +wss://electra.nostr.land +wss://relay.codl.co +wss://nostr.koning-degraaf.nl +wss://relay.mrjohnsson.net +wss://nostr.thank.eu +wss://relay.stonez.me +wss://relay.nostr.distrl.net +wss://relay.valera.co +wss://api.semisol.dev +wss://nostr.lol +wss://relay.shitforce.one +wss://n-word.sharivegas.com +wss://lamp.wtf +wss://nostr.bitcoinpuertori.co +wss://nostr-01.bolt.oberver +wss://3d515c5277e9.ngrok.io +wss://nostr.xmrk.mooo.com +wss://alphapanda.pro +wss://relays.world +wss://universe.nostrich.landlangzh +wss://arnostr.permadao.io +wss://relay.chenxixian.cn +wss://universe.nostrich.landlangzhlangen +wss://v2r.chenxixian.cn +wss://nostr-relay-test.nokotaro.work +wss://universe.nostrich.landlangjalangen +wss://nostr.risa.zone +wss://relay.nosbin.com +wss://translate.argosopentech.com +wss://edennostr.land +wss://nostr.kawagarbo.xyz +wss://nostr.member.cash +wss://ch1.duno.com +wss://nostream-production-b80e.up.railway.app +wss://relay1.nostrich.cloud +wss://relay.t5y.ca +wss://nostr.zhongwen.world +wss://nostr.p2sh.co +wss://nostr.thomascdnns.com +wss://nostream.simon.snowinning.com +wss://relay.nostr.blockhenge.com +wss://nostr.buythisdip.com +wss://nostrua.com +wss://relay.bigred.social +wss://lingoh.dev +wss://nostr.poster.place +wss://nostr.geekgalaxy.com +wss://oarnx6xdrq5mygfdrbmzsvh3is3holefpz2x4qwbopwcicwd63gcivid.onion +wss://relay.nostropolis.xyz +wss://nostream-production-ba43.up.railway.app +wss://nostr.sabross.xyz +wss://relay.nvote.co +wss://nostrati.com +wss://cloudnull.land +wss://nostr.frennet.xyz +wss://nostr.wine.com +wss://nostr.sactiontwo.org +wss://nostr.liberty.fans +wss://nostr.primz.org +wss://btc-italia.online +wss://homenode.local:4848 +wss://nostr.frennet.xyzl +wss://relay.roosoft.com +wss://rasca.asnubes.art +wss://nostr.bitcoin.sexanewlycre +wss://nostr.barf.bz +wss://nostr.middling.mydns.jp +wss://relay.xuzmail.com +wss://no-str.wnhefei.cn:28443 +wss://quirky-bunch-isubghsvoi26fbbt3n7o.wnext.app +wss://nostr.fennel.org:7000 +wss://nostr.0ne.day +wss://nostr.vpn1.codingmerc.com +wss://nostr.jacany.com +wss://nostream.lucas.snowinning.com +wss://relay.beta.fogtype.com +wss://nostr.zue.news +wss://nostream.madbean.snowinning.com +wss://nostr2.rbel.co +wss://relay.1bps.io +wss://nostream-relay-nostr.831.pp.ua +wss://zee-relay.fly.dev +wss://nostrrelay.geforcy.com +wss://relay.nostr.jhot.me +wss://nostr.itredneck.com +wss://nostr.h3y6e.com +wss://relay.bitcoiner.social +wss://hos.lol +wss://iris.to +wss://nostr-pub.senisol.dev +wss://nostr-pub.wellirder.net +wss://bitcoinforthe.lol +wss://relav.nostr.info +wss://3e32-200-229-144-129.ngrok.io +wss://nostr-relay.hzrd149.com +wss://nostr-world.h3z.jp +wss://nostr.thesamecat.io +wss://nostr.compile-error.net +wss://relayable.org +wss://mostra.milou.lol +wss://nproxy.cc +wss://nostr.bitmatk.io +wss://coracle.social +wss://umbrel.local:4848 +wss://nostr.ownbtc.online +wss://wss.nostrgram.co:444 +wss://nostr.minimue81.selfhost.co +wss://relay.current.fy +wss://nostream.nostr.parts +wss://nostr.zebede.cloud +wss://nostrwhoop.ph +wss://relay.nostrified.org +wss://nproxy.zerologin.co +wss://nostr.pcdkd.fyi +wss://relay.kongerik.et +wss://nostr.eden.land +wss://nostr.retroware.run.place +wss://relay.humanumest.social +wss://bhagos.org +wss://hushvault.ie +wss://nostream-production-9458.up.railway.app +wss://nostr.nakamotosatoshi.cf +wss://globals.fans +wss://nostr.cruncher.com +wss://nostr.global.fan +wss://relay.nostr.snblago.com +wss://nostr.nokotaro.com +wss://ostr-1.afarazit.eu +wss://relay.zhix.in +wss://stats.nostr.band +wss://nostr.fine +wss://vxlw4rlg7go34ol43g4gxbvfu4txdzjauquvnbptzwjflezs3vik55id.onion +wss://big.fist.black +wss://universe.nostrich.landlangzhlangja +wss://relay.plebz.space +wss://nostrich.land +wss://relay.mynostr.fun +wss://swiss-enigma.ch +wss://nostr1.current.fyi +wss://relay.atlas.nostr.land +wss://nostr.band +wss://n.wingu.se +wss://nostr.jmdtx.com +wss://nostrproxy-1.f7z.io +wss://nostr.ch +wss://roundrockbitcoiners.com +wss://nostr.sept.ml +wss://srelay.roli.social +wss://nostr.monostr.com +wss://nostr.dojotunnel.online +wss://nostrica.dojotunnel.online +wss://nostr.shadownode.org +wss://thes.ai +wss://rsslay.wss +wss://nostr.vol.io +wss://nostrgram.co +wss://habla.news +wss://runningnostr.lol +wss://mostr.pub +wss://relay.example2.com +wss://profiles.f7z.io +wss://nostr.adpo.co +wss://jp-relay-nostr.invr.chat +wss://nostr.anchel.nl +wss://mutinywallet.com +wss://relay.nostrbr.online +wss://filter.eden.nostr.land +wss://relay.nostrdocs.com +wss://relay.nostr.lucentlabs.co +wss://n.xmr.se +wss://nostr.relayer.rs +wss://monad.jb55.com:8080 +wss://nostr.watch +wss://universe.nostrich.landlangenlangzhlangja +wss://nostr.asdf.mx +wss://ts.relays.world +wss://arc1.arcadelabs.co +wss://stealth.wine +wss://nostr.bg +wss://really.nostr.bg +wss://realy.nostr.bg +wss://relai.nostr.bg +wss://relay-verified.deschooling.us +wss://4.up.railway.app +wss://nostr-relay.aapi.me +wss://nostr-z9tc.onrender.com +wss://nostrich.site +wss://nostr.ginuerzh.xyz +wss://310b-200-229-144-129.ngrok.io +wss://nostr.aste.co +wss://black.nostrcity.club +wss://nostr.guru +wss://nostrica.com +wss://relay.leesalminen.com +wss://nostr.shroomslab.net +wss://meta-relay-beta.nostr.wirednet.jp +wss://nostr.reamde.dev +wss://nostr.africa +wss://powrelay.xyz +wss://rsslay.data.haus +wss://nostr.danvergara.com +wss://nostr.one.re +wss://dev2.hazilitt.fiatjaf.com +wss://nostr-2.zebdeee.cloud +wss://zerosequioso.com +wss://brb.io.relay +wss://relay.realsearch.cc +wss://nodestr.fmt.wiz.biz +wss://r.alphaama.com +wss://nostr.relay-wlvs.space +wss://relay.21spiritis.io +wss://nostr.pinkanki.org +wss://nostr.damus.io +wss://fin-nostr.seekdisruption.com +wss://relay.nostr.lu.ke +wss://nostr.rdfried.com +wss://nostr.trustbtc.org +wss://nostr.verymad.net +wss://relay.damus.info +wss://relays.nostrplebs.com +wss://nostr.688.org +wss://15171031.688.org +wss://dgi4mb7antpcmrx4rynm6xq52xzt5duvxa4iwucq4mszgpz6smrjajqd.onion +wss://mastodon.cloud +wss://nostr.ch3n2k.com +wss://nostr.forecastdao.com +wss://nostr.nostrelay.org +wss://nostr.robotesc.ro +wss://nostr.test.aesyc.io +wss://nostr.web3infra.xyz +wss://nostrrelay.maciejz.net +wss://nostrsxz4lbwe-nostr.functions.fnc.fr-par.scw.cloud +wss://nostrpurple.com +wss://rsslay.ch3n2k.com +wss://nos.qghs.in +wss://nostr.nordlysln.net:3241 +wss://nostr.net.in +wss://relay.rip +wss://universe.nostrich.landlangenlangja +wss://wmv-vm.local:4848 +wss://relay.austritch.net +wss://relay.oxtr.dev +wss://rwlay.bigred.social +wss://relay.lexongtonbitcoin.org +wss://knostr.neutrine +wss://nostr.online +wss://filter.stealth.wine +wss://nostream-production-5895.up.railway.app +wss://nostr.stereosteve.com +wss://relay01.apus.network +wss://test.nostr.0x50.tech +wss://nostr.0x50.tech +wss://nostr.256k1.dev +wss://nostr.malin.onl +wss://jqiwgflfw4dezjsy42frompmknrlcfazoiyngftgknj7yrmnhtobd7id.local +wss://b.ayit.org +wss://nostrelay.rajabi.ca +wss://off20chain.pub +wss://nostr.milou.land +wss://nostr.primedomain.fr +wss://nostr.theblockreward.com +wss://anon.computer +wss://relay.hamnet.io +wss://nostramsterdam.vpx.moe +wss://global-relay.cesc.trade +wss://btcpay.kukks.org +wss://relay.cent2sat.com +wss://nostr.mnethome.de +wss://nostream.sh4.red +wss://klockenga.social +wss://nostream.megadope.snowinning.com +wss://nostr.cvilleblockchain.org +wss://nostr.bitocial.xyz +wss://nostr.bitcoiner.socail +wss://nostr-relay.eniehack.net +wss://greenart7c3.dedyn.io +wss://nostr.data.naus +wss://8.tcp.ngrok.io:19607 +wss://relay.nostr24.com +wss://d463rbo7dgbfuxvvxpory2og2etl4gttfzmqcixdq7rpts47lpgolkyd.onion +wss://dublin.saoirse.dev +wss://www.weixin.com +wss://nostr-rs-relay.cryptoassetssubledger.com +wss://nostr.kojira.net +wss://nostr.fan +wss://nostr.pk +wss://getalpy.com +wss://billert.xyz +wss://circle-ay.info +wss://jawsh.xyz +wss://walletofsatoshi.com +wss://ogblock.xyz +wss://nodestrich.com +wss://kunigaku.gith +wss://nostr.21ideas.org +wss://133332.xyz +wss://asats.io +wss://nostrchack.me +wss://chalow.net +wss://cashu.me +wss://tsukemonogit.git +wss://h3y6e.com +wss://elder.nostr.land +wss://xmr.rocks +wss://nostr.build +wss://mofumemo.com +wss://kpherox.dev +wss://sb.nostr.band +wss://tyiu.xyz +wss://welkinhere.githu +wss://ocha.one +wss://in.tips +wss://vitorpamplona.co +wss://fiatjaf.com +wss://akiomik.github.io +wss://stacker.news +wss://nvk.org +wss://lordkno.ws +wss://nostr.indus +wss://lotdkno.ws +wss://nosutora.com +wss://milou.lol +wss://mostr.pu +wss://dergigi.com +wss://shirehodl.com +wss://cash.app +wss://h3z.jp +wss://murachue.cytes.net +wss://snowcait.gith +wss://jb55.com +wss://nodeless.io +wss://orange-crush.com +wss://nostr.com.au +wss://oooxxx.ml +wss://plebs.place +wss://ahr999.com +wss://penpenpng.github.io +wss://nostrpurple.co +wss://thank.eu +wss://weep.jp +wss://tigerville.no +wss://nostrcheck.me +wss://frenstr.com +wss://ln.tips +wss://ryumu.dev +wss://honeyroad.store +wss://harlembitcoin.com +wss://f7z.io +wss://relay.fan +wss://nisshiee.org +wss://www.lopp.net +wss://getalby.com +wss://heguro.com +wss://wil.bio +wss://b.tc +wss://nodedttich.com +wss://relay.taldra.in +wss://relat.nostrica.com +wss://nostr.boring.surf +wss://nostr.raitisoja.net +wss://nostr.astrox.app +wss://nostr.mjex.me +wss://slick.mjex.me +wss://nostr.hrmb.org +wss://relay.semaphore.life +wss://rss.nostr.band +wss://63ragcfwb5xhoe5gfflazfyrde3qjdo73cblhhmnbviizowdo2q5haid.onion:5051 +wss://nostrblip.app +wss://relay.vanderwarker.family +wss://relay-local.cowdle.gg +wss://relay.damus.com +wss://relay.reeve.cn +wss://relay.strfry.net +wss://relay.alxgsv.com +wss://nostrv0l.io +wss://relay.nostrula.com +wss://release.nostr.band +wss://nostr.k3tan.com +wss://nostr.bitcoin.social +wss://thesimplekid.space +wss://relay.ypcloud.com +wss://at.nostrwork.at +wss://nostream.dev.kronkltd.net +wss://nostr-relay.xbytez.io +wss://relay.nostr.io +wss://relay.nvote.co:443 +wss://relay.cryptoculture.com +wss://rjj6ejkihilniytxs56qrgtttgcfnnjvbii6vaas6jzppcmekd63ugad.local +wss://puravida.nostr.land.com +wss://bitcoinmaximalist.online +wss://wine.nostr +wss://nostr.messagepush.io +wss://nostrich.love +wss://relaynostrplebs.com +wss://hamstr.to +wss://yosupp.app +wss://snort.social +wss://relay.nostr.gt +wss://nost.ratchat.nl +wss://nostr.chrissmith.site +wss://i.relay.boats +wss://nostr.wineto +wss://nostr.eluc.ch +wss://nostrplebs.com +wss://nostr.tools.global.id +wss://nostr.rocketnode.space +wss://relay.roli.social +wss://bitcoin.nostr.com +wss://relay.badgr.space +wss://nostriches.club +wss://nostr-check.me +wss://nostrelay.nokotaro.com +wss://rbr.bio +wss://rly.bopln.com +wss://6amyhf3sjvgxe5qzbx4xn52pcnqresdmi7szxurp6umkvz6mthxjdcad.onion +wss://6amyhf3sjvgxe5qzbx4xn52pcnqresdmi7szxurp6umkvz6mthxjdcad.local +wss://20nos.lol +wss://test.theglobalpersian.com +wss://nostr.exposed +wss://nostr-pub.liujiale.me +wss://nostream.frank.snowinning.com +wss://nstrs.fly.dev +wss://eospark.com +wss://relay.nosterplebs.com +wss://nproxy.kristapsk.lv +wss://nostr.universalname.space +wss://relays.snort.social +wss://nostr.how +wss://kukks.org +wss://nostr.dutch.cryptonews +wss://relay.cryptojournaal.net +wss://relay.dutch.cryptonews +wss://nostr.cryptojournaal.net +wss://no-str.wnhefei.cn +wss://www.131.me +wss://rsr.uyky.net +wss://nostr-relay.ie9.org +wss://nostr.fmt.wis.biz +wss://nostr.exotr.dev +wss://nostr.merrcurr.com +wss://realy.damus.io +wss://nerostr.xmr.rocks +wss://rkdgwzgvcgrciemlnfsxqgyrv5whgpw44s6zycokmuchpq4ucflgjtqd.local +wss://nostr.simplex.icu +wss://ralay.damus.io +wss://relay.kronkitd.net +wss://nostr.info +wss://nostr.lnnodeinsight.com +wss://nostr.truckenbucks.com +wss://brt.io +wss://nostr.lingoh.dev +wss://relay.nor.st +wss://nostr-relay.inmarkets.com +wss://wiz.biz +wss://nostream.git +wss://nostr.dpbu.de +wss://wcl2meyp236fa3dmfzfyq6aacbdoixrlocb6zozjs6xklxizschj2did.local +wss://relay.nostr.co.jp +wss://relap.orzv.workers.dev +wss://nostr.bitcoiner.socia +wss://eden.nosrt.land +wss://strfry.cryptocartel.social +wss://nostr.citizenry.technology +wss://universe.nostrich.landlang +wss://nostr.inprivate.network +wss://relay.snort.test +wss://lpkue6jtz3pnp7zok4jwlct4n3mzuffsfrpagiffsuplyaswhdtmpoid.local +wss://nostr.rezhajulio.id +wss://lnbits.eldamar.icu +wss://nostr.freefrom.fi +wss://yael.at +wss://rain8128.github.io +wss://badges.page +wss://latam1-nostr.stealthy.co +wss://nostr.relay.se +wss://nfdn.testnet.dotalgo.io +wss://relav.nostr3.io +wss://ofchain.pub +wss://nostr.fmt.wiz.bi +wss://relay.gui.dog +wss://eden.nostry.land +wss://nostr.relay.limo +wss://relay.nostrichs.org +wss://20nostr.mom +wss://relay20damus.io +wss://nostr.sandwich.pro +wss://sq.qemura.xyz +wss://nostr-pub.seminol.dev +wss://nostr.eunundzwanzig.space +wss://nostr.bitcoinet.social +wss://nostr-relay.untether.me +wss://relay.nostr.pub +wss://nostr.100p.org +wss://7tdom3xuus7ekv423ul46w3j43zyixjj54yoe62bndpcrgii3adeppid.local +wss://nostr.gram +wss://nostr.videre.net +wss://nostr-2.zebedee.cloudwss +wss://nostr-pub.wellorder.netwss +wss://sonzai.net +wss://snort.fail +wss://ppavybjpqjft5slnpeovehbegomhwtvvxtesvwwdrfndz6qe2c5kf2ad.local +wss://nostr.land +wss://relay.nvote.com +wss://purevida.nostr.land +wss://nostr.bitcoiner.soical +wss://nostr.inosta.co +wss://nas.lol +wss://wss.nostr.milou +wss://nostr.cheesebot.org +wss://eyeswideshut.ath.cx +wss://test.relays.world +wss://relay.snort.com +wss://relay.nostrplebs.co +wss://nostr.kimi.im +wss://nostrum.com +wss://wss.nostr.wine +wss://relay.usenostr.org +wss://paladium.my:4848 +wss://umbrel.home.local:4848 +wss://nostr.metamadeenah.com +wss://lnbits.sdbtc.org +wss://relay.nostr.mutinywallet.com +wss://relay.theglobalpersian.com +wss://relay1.easymeta.app +wss://relay.nostris.online +wss://s1.wonder3.org +wss://eden.nostraland +wss://byc.klendazu.com +wss://ephemerelay.mostr.pub +wss://7si6co27cvaw5yjyx6asvxfmaw5ah2arywwgrem4y5svi5ntskoeb5id.onion +wss://nwmdev.com +wss://nostro.online +wss://nostr.massimux.com +wss://nostr.glate.ch +wss://nostr.openordex.org +wss://nostr.schorsch.fans +wss://nostr-relay-dev.nisshiee.org +wss://ibz.me +wss://alexandernostrplebs.com +wss://fishbanananostrplebs.com +wss://relay.nostrcheck.com +wss://nostr.roli.io +wss://nostr.net.za +wss://nostr.worldkey.io +wss://nostr-pub.welloorder.net +wss://nostr.actin.io +wss://nostrzebedee.cloud +wss://nostr.zclub.app +wss://nostr.13x.sh +wss://nostr.totient.xyz +wss://nostr1.federated.computer +wss://nostr.zhix.in +wss://nostr.vdstruis.com +wss://caro-relay.fiatjaf.com +wss://nostr.winewss +wss://notstro.wine +wss://nostr.btc-library.com +wss://nostr.phenomenon.space +wss://nostr.octr.dev +wss://nostr.impervious.live +wss://nostr.plebs.space +wss://iefan.tech +wss://w3ird.tech +wss://yunginter.net +wss://nostr.coach +wss://sleepy.cafe +wss://freespeechextremist.com +wss://nostr.uselessshit.com +wss://liberdon.com +wss://eveningzoo.club +wss://gleasonator.com +wss://mindly.social +wss://ottawa.place +wss://noagendasocial.com +wss://poa.st +wss://misskey.io +wss://misskey.cf +wss://nostr.bybieyang.com +wss://mastodon.online +wss://toad.social +wss://best-friends.chat +wss://universeodon.com +wss://mastodon.world +wss://mastodon.social +wss://nostr.vulpem +wss://nicecrew.digital +wss://front-end.social +wss://nostr-relay.wellorder.net +wss://relays.pro +wss://rsslay.sovbit.host +wss://seal.cafe +wss://social.6bq.de +wss://universe.nostrich.landlangjalangzh +wss://social.xenofem.me +wss://mi.hibi-tsumo.com +wss://mstdn.social +wss://mas.to +wss://relay.rebelbase.site +wss://pixelfed.social +wss://digitalcourage.social +wss://ruby.social +wss://cr8r.gg +wss://nijimiss.moe +wss://returtle.com +wss://lor.sh +wss://toot.community +wss://mstdn.jp +wss://relay.berserker.town +wss://nostr.rajabi.ca +wss://fosstodon.org +wss://misskey.takehi.to +wss://izj3isbk3pmade74ontdijodhehsytnw2iokdhh6k3flk4mq2pau6sid.onion +wss://mastodon.scot +wss://nostriches.org +wss://aus.social +wss://relay.nostr.amane.moe +wss://romancelandia.club +wss://stonez.me +wss://merrcurr.com +wss://nostr.ist +wss://onprem.wtf +wss://fedibird.com +wss://s2.wonder3.org +wss://freespech.casa +wss://disabled.social +wss://relay.house +wss://nostr-pub.welllorder.net +wss://social.teamb.space +wss://infosec.exchange +wss://blockedur.mom +wss://progressivecafe.social +wss://mstdn.ca +wss://c.im +wss://mefi.social +wss://basebitcoinplebs.place +wss://med-mastodon.com +wss://ohai.social +wss://defcon.social +wss://pxlmo.com +wss://zlocur7ctbds4qsdswb3qpkp6n2e2ywqne2fp2tdn4fqubpudfiyxwid.local +wss://thebag.social +wss://mstdn.party +wss://relay.serpae.xyz +wss://clew.lol +wss://qoto.org +wss://relay.exchange +wss://nostr.relayable.org +wss://mastodon.coffee +wss://kmy.blue +wss://home.social +wss://detmi.social +wss://brighteon.social +wss://nostr.nom +wss://theblower.au +wss://astral.nostr.land +wss://beige.party +wss://orangepill.dev +wss://redgreenblue.click +wss://nostr.fredix.xyz +wss://nostr.essydns.ca +wss://nostr.private.network +wss://nostr.member.cas +wss://nostr-rely.digitalmob.ro +wss://relay.nostr +wss://relay.current +wss://no-str.or +wss://pl.gamers.exposed +wss://studentchadpolytechnic.com +wss://scicomm.xyz +wss://relay.alien-sos.gov +wss://masto.es +wss://spinster.xyz +wss://parallels-parallels-virtual-platform.local:4848 +wss://relay.daums.io +wss://kolektiva.social +wss://mastodonapp.uk +wss://convo.casa +wss://sfba.social +wss://techhub.social +wss://leafposter.club +wss://nrw.social +wss://mastodon.uno +wss://handon.club +wss://social.vivaldi.net +wss://nostr.goller.net +wss://minazukey.uk +wss://mstdn.beer +wss://expressional.social +wss://paid.nostr.0x50.tech +wss://mstdn.nere9.help +wss://relay.nostr.ai +wss://relay.noswss +wss://relay.nwss +wss://furry.engineer +wss://uselessshit.co +wss://kosmos.social +wss://mynostr.io +wss://premis.one +wss://social.tchncs.de +wss://retro.pizza +wss://pearl-mount-showed-fishing.trycloudflare.com +wss://kpa4k6acxzjv2m2p72keftbpaymwpq2h67jqnin3d4y3djxyheuifoqd.onion +wss://gm7.social +wss://relays.nostr.info +wss://chitter.xyz +wss://hackers.town +wss://anarchism.space +wss://relay.nostrica +wss://halifaxsocial.ca +wss://asimon.org +wss://nostr.blipme.add +wss://troet.cafe +wss://octodon.social +wss://m.cmx.im +wss://filename-ambassador-distance-mountains.trycloudflare.com +wss://nostr.getgle.org +wss://relay.froth.zone +wss://novoa.nagoya +wss://relay.dispute.systems +wss://clubcyberia.co +wss://thechimp.zone +wss://coolsite.win +wss://puravida.nostra.land +wss://shelter.local:1111 +wss://det.social +wss://nostr-2.zebee.cloud +wss://chaosfem.tw +wss://nostr.v01.io +wss://social.kechpaja.com +wss://mastodon.green +wss://plebchain.nostr +wss://plebchain.nostr.land +wss://relay.onsats.org +wss://u5epuanp2fbie4phw6zekzna6zotvsffji4td4ee7iwgwdxlz4kwqqad.onion:5051 +wss://3ddc8bbee6db.ngrok.app +wss://b4968f09859e.ngrok.io +wss://genserver.social +wss://masto.ai +wss://hachyderm.io +wss://shitpost.cloud +wss://oldbytes.space +wss://mastodon.ie +wss://baraag.net +wss://nostr.dvdt.dev +wss://relay.com.de +wss://nostr.rbel.co +wss://bologna.one +wss://nostr-relay.app +wss://kagamisskey.com +wss://nostr-rs-relay.phamthanh.me +wss://nostr.bcmp.com +wss://blogstack.io +wss://rly.nostrkid.com +wss://mastodon.bida.im +wss://freeatlantis.com +wss://pieville.net +wss://climatejustice.rocks +wss://relay.mynostr.id +wss://relay.farscapian +wss://relay.blogstack.io +wss://orwell.fun +wss://misskey.04.si +wss://sushi.ski +wss://nostr.milou.lo +wss://pawoo.net +wss://tooter.social +wss://mastodon.sdf.org +wss://nostr.com +wss://misskey.design +wss://macaw.social +wss://relay.nostr.inforelay.nostr.band +wss://pub1.southflorida.ninja +wss://strawberry-pudding.net +wss://mastodon-japan.net +wss://nostream.0x50.tech +wss://multiplextr.coracle.social +wss://pleroma.skyshanty.xyz +wss://uxxq6b2enojvflhkrzsg4erakd5rrb7v2cql4m4pspj4xtqwyli47rid.local +wss://masto.deoan.org +wss://replay.damus.io +wss://melhorque.com.br +wss://nostr1.actn.io +wss://nostr.bolt.fun +wss://relay.vtbmoyu.com +wss://eosla.comrelay.zeh.app +wss://bikeshed.party +wss://nostr.xanny.family +wss://milker.cafe +wss://nostr.give.africa +wss://relay.got-relayed.com +wss://cum.salon +wss://puravid.nostr.land +wss://soc.punktrash.club +wss://seafoam.space +wss://h4.io +wss://nostr.swiss.enigma.ch +wss://toot.cafe +wss://sneed.social +wss://newsie.social +wss://indg.club +wss://xoxo.zone +wss://relay.nostr.bitcoiner.social +wss://relay.snort.band +wss://socel.net +wss://social.coop +wss://postpandemicparty.org +wss://merveilles.town +wss://search.nostr.wine +wss://search.nos.today +wss://mstdn-huahin.com +wss://eden.nostr.la +wss://nostrja-kari-nip50.heguro.com +wss://relay.gems.xyz +wss://plnetwork.xyz +wss://nostr.swiss-enigma.sh +wss://geofront.rocks +wss://toot.io +wss://indieweb.social +wss://mastodon.content.town +wss://awaymessage.club +wss://relay.intify.io +wss://mstdn.science +wss://maniakey.com +wss://otadon.com +wss://mstdn.guru +wss://misskey.noellabo.jp +wss://hessen.social +wss://mastodon.top +wss://ipv6.nostr.wirednet.jp +wss://relay.nostr-relay.org +wss://cum.camp +wss://nebbia.fail +wss://current.fyi +wss://bae.st +wss://lolison.network +wss://ioc.exchange +wss://bylines.social +wss://decayable.ink +wss://nostr.unitedserializer.com +wss://urbanists.social +wss://dark-elves.social +wss://writing.exchange +wss://nostr.rikmeijer.nl +wss://misskey.social +wss://ht.nixre.net +wss://mathstodon.xyz +wss://t7jvqwu35hneszx7fihsprbcpwonlcfnsjr4xtn6shqgwbv324w4gdid.local +wss://abla.news +wss://muenchen.social +wss://homeserver.drake-carp.ts.net:4848 +wss://snailedit.social +wss://mastodon.gamedev.place +wss://tech.lgbt +wss://mast.lat +wss://econtwitter.net +wss://veganism.social +wss://btclolap6mm4tl37huslk6j76enq7qxaj2kwq7w6cdr5ros56eletcqd.onion +wss://toot.cat +wss://nostr.zenon.info +wss://misskey.art +wss://nostr.hushvault.ie:4848 +wss://o6ga6lxnax2z7pgkkenifollohkrv55r36mdzdtofi7d5yyif2f4o5yd.onion:5051 +wss://20nostr-pub.wellorder.net +wss://ecoevo.social +wss://nostr.zerofiat.world +wss://nostr.atitlan.io +wss://detroitriotcity.com +wss://rollenspiel.social +wss://paquita.masto.host +wss://literatur.social +wss://dave.st.germa.in +wss://sunny.garden +wss://nostrpro.xyz +wss://relay.getalby.com +wss://misskey.systems +wss://mamot.fr +wss://social.anoxinon.de +wss://relay.nostr.social +wss://mindmachine.org +wss://microblog.club +wss://relay.utxo.com +wss://universe.nostrich.landlangth +wss://social.teci.world +wss://social.freetalklive.com +wss://mk.absturztau.be +wss://social.ornella.xyz +wss://left-tusk.com +wss://bofh.social +wss://a11y.social +wss://shroomslab.net +wss://relay2.nostr.vet +wss://mugicha.club +wss://laserbeak.local:4848 +wss://annihilation.social +wss://humble.cafe +wss://nostr.relay.damus.io +wss://nostr.milol.lol +wss://nostr.blimpme.app +wss://noc.social +wss://wetdry.world +wss://nostr.taxi +wss://nostr.21-bitcoin.org +wss://relay.llevotu-bitcoiners.info +wss://pl.kitsunemimi.club +wss://djsumdog.com +wss://citadel.local:4848 +wss://federate.blogpocket.com +wss://chaos.social +wss://mastodon.me.uk +wss://oxtr.dev +wss://misskey.cloud +wss://varishangout.net +wss://lacosanostr.com +wss://willem.currycash.net:4848 +wss://lightniningrelay.com +wss://queer.party +wss://lightning.relay.com +wss://mastodon.nl +wss://purplepag.es +wss://cupoftea.social +wss://bitcoiner.socialwss +wss://relay.current.io +wss://sigmoid.social +wss://wandering.shop +wss://braydmedia.de +wss://quey.la +wss://nostr.zebeedee.cloud +wss://nostr-2.zebeedee.cloud +wss://artsio.com +wss://nostr1676031941328.app.runonflux.io +wss://arsip.ddns.net +wss://relay.nostrcitadel.org +wss://relay.nostr-citadel.org +wss://nostr-citadel.org +wss://blastr.f7z.io +wss://nostr.myowndamnnode.com +wss://snowdin.town +wss://nostr.zxcvbn.space +wss://relay.notmandatory.org +wss://bitcoin.social +wss://friendsofdesoto.social +wss://zirk.us +wss://digipres.club +wss://nostr-test.elastos.io +wss://nostr.onsat.org +wss://damus.relay.io +wss://beefyboys.win +wss://nostrija-kari.heguro.com +wss://froth.zone +wss://ns.penseer.com +wss://relay.nostrical.com +wss://nostr.hoshizora.ch +wss://kunigaku.github.io +wss://nostr.shino3.net +wss://umbrel.tailbb128.ts.net:4848 +wss://tailbb128.ts.net:4848 +wss://fedi.twoshortplanks.com +wss://filter.nostr.winebroadcasttrue +wss://nostr.doufu-tech.com +wss://relay.hodl.haus +wss://pgh.social +wss://coeditor-congested.fractalnetworks.co +wss://mastodon.podaboutli.st +wss://frenfiverse.net +wss://nostr.f4255529.fun +wss://nostream.megadope.snowinning +wss://kiritan.work +wss://forever21.lol +wss://zitron.net +wss://mastodon-swiss.org +wss://relay1.current.fyi +wss://nostr.plebs.com +wss://multiplextr.corocal.social +wss://nostr.zedebee.cloud +wss://nostr.mutinywallet.comisntbanned.addthatonesotheycangetnotesrelayedtotherestofthenetworkfrominsideofchina +wss://mastodon.mit.edu +wss://relaynostrati.com +wss://relaynostr.band +wss://relaynostr.info +wss://relaychenxixian.cn +wss://relaysnort.social +wss://relaynostr.com.au +wss://nostr-tbd.website +wss://relay.hoshizora.ch +wss://social.balsillie.net +wss://strangeobject.space +wss://relay.nvote20.co +wss://fla.red +wss://urusai.social +wss://chad.polytechnic.com +wss://robo358.com +wss://conxole.io +wss://relay.ohbe.me +wss://nostr.bitcoiner.com +wss://www.mutinywallet.com +wss://x.9600.link:10070 +wss://a11y.info +wss://homelab.host +wss://k65qz57zx4sw24fow2bvchjgmpjqljj4cp3oa7dtpbbprjifsdotggid.local +wss://relay.vtuber.directory +wss://x.9600.link:8000 +wss://nostr.plebs.win +wss://renkontu.com +wss://nost.massmux.com +wss://submarin.online +wss://social.librem.one +wss://relai.kongerik.et +wss://mstdn.io +wss://astral.swiss-enigma.ch +wss://stereophonic.space +wss://relayer.pleb.social +wss://mastodon.nz +wss://nostr.nostr.de +wss://nostr.thezap.club +wss://proxy.shroomslab.net +wss://pfr24mrpxowclhm4y6adu36kbo3erx7gskzyfqqhnhfpdkdmylpfgkyd.local +wss://nostr.hodl.haus +wss://vtdon.com +wss://relay.nost.band +wss://fnxwipsg3lfzij64lvjgmutvkkpd7eo2mr2khxkofyywf3vsvbk73jad.onion +wss://fnxwipsg3lfzij64lvjgmutvkkpd7eo2mr2khxkofyywf3vsvbk73jad.local +wss://md.hugo.nostr +wss://boks.moe +wss://truthsocial.co.in +wss://mastodon.xyz +wss://relay.universalname.space +wss://relay-nostr.wirednet.jp +wss://fedi.pawlicker.com +wss://favcalc.com +wss://rot13maxi.com +wss://awayuki.net +wss://mazinkhoury.com +wss://g0v.social +wss://shpposter.club +wss://a.lufimianet.jp +wss://pura20vida.nostr.land +wss://sotalive.net +wss://sendsats.lol +wss://vida.page +wss://wagvwfrdrikrqzp7h3b5lwl6btyuttu7mqpeji35ljzq36ovzgjhsfqd.onion +wss://uec2cmjauzufrtlq6wq6l2ujfncdvo3suezz423gsvz5xvhehm2mcgid.onion +wss://mastodon-belgium.be +wss://rejecttheframe.xyz +wss://nogood.store +wss://getaiby.com +wss://plebchain.club +wss://nostrverified.com +wss://x.9600.link +wss://n0p0.shroomslab.net +wss://orangemakura.xyz +wss://jamw.net +wss://7ab7qqbj2dw3pjnkoskgsfn4ikqc7orwnkpmcfjmeobw63kf4zgykjid.local +wss://snabelen.no +wss://floss.social +wss://mastoot.fr +wss://kmc-nostr.amiunderwater.com +wss://hodl.camp +wss://xmr.usenostr.com +wss://nostr-pub.wellorn.net +wss://gudako.net +wss://ryona.agency +wss://kafeneio.social +wss://massmux.com +wss://freezepeach.online +wss://nostream-production-f83d.up.railway.app +wss://taobox.pub +wss://mastodon.radio +wss://einundzwanzig.relay.com +wss://journa.host +wss://social.here.blue +wss://toot.wales +wss://staging.nostr.com.se +wss://pleroma.soykaf.com +wss://berserker.town +wss://zapforart.site +wss://forall.social +wss://nostrja-kari.heguro.comyee +wss://higheredweb.social +wss://social.gnuhacker.org +wss://the.hodl.haus +wss://ng4jk6yiqgfczo4wyxszuj7w6jok3fptehu533o3mlzs3vph3dvjfdid.onion +wss://nostr.mtpx.ovh +wss://relay.coollamer.com +wss://glasgow.social +wss://fedi.absturztau.be +wss://nostr.frostr.xyz +wss://relay.runningnostr.lol +wss://relay2.vtuber.directory +wss://nerdculture.de +wss://nostr-relay.untethr.meoperator +wss://nostr.verif-slothy.win +wss://nostr.pub +wss://social.process-one.net +wss://invillage-outvillage.com +wss://otofu.uk +wss://universe.nostrich.landlangenlangpt +wss://mastodon.iriseden.eu +wss://frighteningdeafeningagent.nailuogg.repl.co +wss://obo.sh +wss://relay.hodlhaus.net +wss://rdrama.cc +wss://nostr.packetlostandfound.us +wss://test.relay +wss://mastodonbooks.net +wss://southflorida.ninja +wss://blob.cat +wss://tooting.ch +wss://relay.pineapple.pizza +wss://relay.nostr.directory +wss://relar.nostr.bg +wss://reisen.church +wss://relay.honk.pub +wss://nostr.rsfriedl.com +wss://relay.nort.social +wss://1611.social +wss://the.hodl.house +wss://relayable.com +wss://fedi.syspxl.xyz +wss://nostrpub.yeghro.site +wss://skr5bbrgzfnideglw4cs2iw6au2jm2b7gupocxbmkv5qopo6rcitmiqd.local +wss://a2mi.social +wss://status.relayable.org +wss://toot.blue +wss://btcqspp5dl4rlgl5pomcyv3odfeki7a5zrmjoekyu5vsoqz5bth4e7yd.onion +wss://bitcoinr6de5lkvx4tpwdmzrdfdpla5sya2afwpcabjup2xpi5dulbad.onion +wss://7tdom3xuus7ekv423ul46w3j43zyixjj54yoe62bndpcrgii3adeppid.onion +wss://dnze4ekho2kuiejwatjw5omeprtmdaum2ukok52roiu5rztii3rp2aid.onion +wss://53snncs7vegargpaardbxjnii2oan3xpmbeaf6czwoqa2axz5mvbsjid.onion +wss://fnqdhz3df33da6wxg7jskvumd5rjn3nknln6ecun7uwwysc7vkwkjgid.onion +wss://relay.thefockinfury.wtf +wss://silliness.observer +wss://freak.university +wss://piaille.fr +wss://nostr.weking.tk +wss://f.reun.de +wss://radixrat.com +wss://hostux.social +wss://chat.freenode.net +wss://no.str..cr +wss://nostr.inoata.cc +wss://kappa.seijin.jp +wss://floyds.io +wss://mast.dragon-fly.club +wss://zbd.ai +wss://misc.name +wss://pdx.social +wss://0w0.is +wss://d6jvu2tev2rblkuzgu4ydw2413jizr53j26ut47hxpykvtusbvekhiid.onion +wss://d6jvu2tcv2rblkuzgu4ydw24l3jizr53j26ut47hxpykvtusbvekhiid.onion +wss://drcassone.social +wss://mastodon.energy +wss://rayci.st +wss://ischool.social +wss://nostr.halfway2forever.com +wss://relay.nostr.rocks +wss://nostr.stoner.com +wss://2nodez.com +wss://rs.nostr-x.com +wss://schleuss.online +wss://thrashzone.org +wss://relay.nostrgraph.com +wss://relay.2nodez.com +wss://occult-zuki.com +wss://mastodon.lithium03.info +wss://bigbadpc.local:4848 +wss://social.gr0k.net +wss://nostr.wirednet.jp +wss://relay.plebster.com +wss://bitcoin.nostr +wss://mastodon.im +wss://brb..io +wss://nostr.sandwhich.farm +wss://relay.nos.lol +wss://beta.nostr.v0l.io +wss://eden.nostr.space +wss://powerlay.xyz +wss://rap.social +wss://relay.mutinywallet.com +wss://rogue.earth +wss://600.wtf +wss://klabo.blog +wss://petrikajander.com +wss://tgkzmdd.help +wss://nostr.red +wss://brb.lol +wss://jz2l2bf6f6wssdqwkg7ogthkc5i3ymyiwkaz3tbhff6ro3h3zqddekyd.onion +wss://mstdn.mini4wd-engineer.com +wss://nostr.exposd +wss://nostr.a-ef.org +wss://computerfairi.es +wss://social.targaryen.house +wss://o3o.ca +wss://walkah.social +wss://cosocial.ca +wss://filter.nostr.band +wss://relais.nostrview.com +wss://gigaohm.bio +wss://dobbs.town +wss://bark.lgbt +wss://mastodon.gal +wss://snug.moe +wss://genomic.social +wss://relay.orangepilldev.com +wss://social.sdf.org +wss://social.camph.net +wss://mstdn.poyo.me +wss://nein.lol +wss://nostr.i00.org +wss://kemono.ink +wss://mu.zaitcev.nu +wss://libera.site +wss://ca.hibi-tsumo.com +wss://social.bund.de +wss://xscape.top +wss://social.lol +wss://birds.town +wss://arnostr.com +wss://nostr.33co.de +wss://relay.nostr.lighting +wss://metadata-contacts-relays.pages.dev +wss://webs.node9.org +wss://pleroma.elementality.org +wss://suya.place +wss://livellosegreto.it +wss://peoplemaking.games +wss://nattois.life +wss://typo.social +wss://neutrine.com +wss://ragner-relay.com +wss://wss.nostr.uselessshit.co +wss://wss.nostrue.com +wss://relay.nvote.co:433 +wss://disobey.net +wss://rneetup.com +wss://arnostr.com:8433 +wss://relay.uxto.one +wss://relay.hackerman.pro +wss://thisis.mylegendary.quest +wss://poliversity.it +wss://sats.lnaddy.com +wss://rs2.abaiba.top +wss://rs1.abaiba.top +wss://rs2.abaiba.top.abaiba.top +wss://social.matarillo.com +wss://nostr01.counterclockwise.io +wss://backup.local:4848 +wss://touhou.vodka +wss://mi-wo.site +wss://nostr.f7z.io +wss://alive.bar +wss://strfry.nostr-x.com +wss://mastodontti.fi +wss://nostr.wellorder.net +wss://y.9600.link:8000 +wss://ephemrelay.mostr.pub +wss://byc-italia.online +wss://fissionator.com +wss://stranger.social +wss://eupolicy.social +wss://nostr-desktop.local:4848 +wss://aoir.social +wss://mstdn.plus +wss://nostrproxy.io:3333 +wss://mastodon.hams.social +wss://jorts.horse +wss://metalhead.club +wss://dice.camp +wss://mstdn.y-zu.org +wss://loffchain.pub +wss://mastodon.llarian.net +wss://nostr-relay2.thefockinfury.wtf +wss://2g2jzcfgq5lcrceuq23lmya2drm3ku5qmqimr3bvu3amol55vidctrad.onion +wss://mastodon.au +wss://bgme.me +wss://nostr.badran.xyz +wss://nostr.coincreek.com +wss://nostream-test.up.railway.app +wss://relay.blackthunder.click +wss://relay.grorp.com +wss://atomicpoet.org +wss://iddqd.social +wss://gusto.masto.host +wss://lifehack.social +wss://blorbo.social +wss://freecumextremist.com +wss://13bells.com +wss://rsslay.nostr.netrelay +wss://relay.zerosequioso.com +wss://nauka-relay.herokuapp.com +wss://nostr.nofdeofsven.com +wss://out.of.milk +wss://cawfee.club +wss://r.relay.fan +wss://alo.ottonove891.cf +wss://keinoha.tailnet-0240.ts.net +wss://nostr.paralelnipolis.cz +wss://tuiter.rocks +wss://elizur.me +wss://nostr-dev.newstr.io +wss://discuss.systems +wss://blahaj.zone +wss://mastodon.art +wss://makersocial.online +wss://gamepad.club +wss://nostr.flameofsoul.ru +wss://dmv.community +wss://relay.nostr.com +wss://nostr-desktop.saiga-shark.ts.net:4848 +wss://nostrfoxden.ddns.net:4848 +wss://soc.umrath.net +wss://ravenation.club +wss://oisaur.com +wss://nostr-relay.net +wss://social.mikutter.hachune.net +wss://lewacki.space +wss://fediscience.org +wss://todon.eu +wss://nuccy-nuc7i5bnk.local:4848 +wss://games.gamertron.net:4848 +wss://fiedlerfamily.net +wss://postnstuffds.lol +wss://nostr.planetary.social +wss://worldkey.io +wss://hcommons.social +wss://gymp7qquljs47xbbvs47hkptnyyzegy2jkst26mkjxaciifqffjatqid.onion +wss://rs3.abaiba.top +wss://sudo-nostr.com +wss://satgag.site +wss://nostr.lnbitcoin.cz +wss://relay20nostrplebs.com +wss://2pbkpndvpeebljfvjew6auq63lndzszqnntct5aqfmazslerzxe75kad.onion +wss://dragonchat.org +wss://welcome.nostr.wine +wss://relay.nostr.land +wss://social.linux.pizza +wss://dnppj4kopczovvzvpzmihv2iwe5wt3gbrxjnltjc2zdjpttrdz4owpad.onion +wss://potofu.me +wss://nostrbr.online +wss://mastorol.es +wss://notebook.taild34d0.ts.net +wss://t.aqn.jp +wss://nostr.mutinywallet +wss://wcone.nostr.wine +wss://norden.social +wss://eostagram.com +wss://shigusegubu.club +wss://toot.jkiviluoto.fi +wss://kiwifarms.cc +wss://swiss-talk.net +wss://v532btfg2fb4za2g476a7w23pgpkllc7uq274wqtktwjogt5ynb3ukqd.local +wss://mstdn.maud.io +wss://arc1.arcadelabs.com +wss://nostr.jp +wss://relay-jp.nostr.wirrdnet.jp +wss://climatejustice.social +wss://witter.cz +wss://mastodon.pnpde.social +wss://ttrpg-hangout.social +wss://beehaw.org +wss://thecanadian.social +wss://nostr.fbxl.net +wss://relay.sandwich.farm +wss://nostr.olwe.link +wss://botsin.space +wss://zeroes.ca +wss://photog.social +wss://paid.nostr.lc +wss://free.nostr.lc +wss://test.nostr.lc +wss://gzanlkgurj7zd3psqms3da4vrw4imurnyyzaycfuiiug7elqow7xlayd.onion:5051 +wss://masto.nu +wss://mastodon.uy +wss://bit.relay.center +wss://offchain.relay.center +wss://damus.relay.center +wss://wine.relay.center +wss://eden.relay.center +wss://moth.social +wss://nostr.masmux.com +wss://chrome.pl +wss://mastodon.ktachibana.party +wss://ak.kawen.space +wss://mementomori.social +wss://relay.s3x.social +wss://lnbits.michaelantonfischer.com +wss://yof23ggqmert72c5wcl5qglphapy3o2xjdedtkbrn2dt5rbae2s7f6qd.onion +wss://relay.snort.socail +wss://post.lurk.org +wss://yiff.life +wss://q3zaylwjjhq77yzx34lbydz26szzjljberwetkjgxgsapcekrpjzsmqd.onion +wss://lnbits.b1tco1n.org +wss://welcome.nostr.relay +wss://sound-money-relay.denizenid.com +wss://carnivore-diet-relay.denizenid.com +wss://africa.nostr.joburg +wss://nostr.jolt.run +wss://nostr.chainbits.co.uk +wss://ithurtswhenip.ee +wss://nostr.cloudversia.com +wss://relay1.east.us.nostr.btron.io +wss://ca.orangepill.dev +wss://pdx.land +wss://linh.social +wss://okla.social +wss://androiddev.social +wss://spore.social +wss://mastodo.fi +wss://kabedon.space +wss://nost.inosta.cc +wss://relay2cdamus.io +wss://nostr.openhoofd.nl +wss://dragonscave.space +wss://genart.social +wss://dewp.space +wss://layer8.space +wss://qou7zzll2mxx2ehl73n6pptmhizl5b3entowljlin3sqhcvltxdtlmad.onion:5051 +wss://nostr.wines +wss://relay.snort.relay.ryzizub.com +wss://pixelfed.de +wss://nostr.holyscapegoat.com +wss://nostr.einunzwanzig.space +wss://nostr.hifish.org +wss://colearn.social +wss://topspicy.social +wss://mastodon.neat.computer +wss://relay.nostr.hach.re +wss://nostr.dakukitsune.ca +wss://7ab7qqbj2dw3pjnkoskgsfn4ikqc7orwnkpmcfjmeobw63kf4zgykjid.onion +wss://esq.social +wss://famichiki.jp +wss://tribe.net +wss://masto.nobigtech.es +wss://umbrel-nuc.local:4848 +wss://mastodon.cocoasamurai.social +wss://debian.taildd32b.ts.net:4848 +wss://vlt.ge +wss://relay.johnnyasantos.com +wss://snort.relay.center +wss://nb.relay.center +wss://waag.social +wss://concentrical.com +wss://stat.rocks +wss://oransns.com +wss://relay-jpp.nostr.wirednet.jp +wss://oc.todon.fr +wss://jundow.gitlab.io +wss://neurodifferent.me +wss://jazztodon.com +wss://nostrich.friendship +wss://indieauthors.social +wss://werunbtc.com +wss://frogtalk.lol +wss://pop-os.local:4848 +wss://fediver.de +wss://d6qxo55dhms6revgrmbindvb5ejd3gw5hrji7ylkm6khghii3hjs3uyd.onion +wss://eldritch.cafe +wss://karlsruhe-social.de +wss://social.yl.ms +wss://nostr.mycloudhouse.duckdns.org +wss://nostr.otc.sh +wss://nya.social +wss://relay2.nostrchat.io +wss://relay1.nostrchat.io +wss://nostrja-world-relays-test.heguro.com +wss://ndk-relay.local +wss://reespeech.casa +wss://pleroma.atyh.cc +wss://lawfedi.blue +wss://akkoma.jasminetea.uk +wss://peeledoffmy.skin +wss://plush.city +wss://astrodon.social +wss://samenet.social +wss://toot.bike +wss://mi.yukioke.com +wss://social.growyourown.services +wss://mastodon.nu +wss://lou.lt +wss://functional.cafe +wss://relaydamus.io +wss://coma.social +wss://social.fbxl.net +wss://biplus.social +wss://toots.matapacos.dog +wss://psychoet.ml:3250 +wss://danserver.equipment +wss://nostr.lacrypta.com.ar +wss://wonkodon.com +wss://nostr.seankibler.com +wss://autistics.life +wss://cambrian.social +wss://rvqkqr5kl3dvvxyn67rfowcnvoflx4zby5tjbysavym4ycckti4dbjyd.onion +wss://swiss.nostr.lc +wss://snac.saifulh.online +wss://loma.ml +wss://nostr.privoxy.io +wss://366.koyomi.online +wss://mastodon.stormy178.com +wss://podcastindex.social +wss://bitcoiner.socia +wss://fedi.ml +wss://replayable.org +wss://nostr.schroomslab.net +wss://nostr.global.fans +wss://relay.weedstr.net +wss://vocalodon.net +wss://relay.nostr.bandadd +wss://nostr.oxtr.devadd +wss://jarvis.taild68e2.ts.net:4848 +wss://t7jvqwu35hneszx7fihsprbcpwonlcfnsjr4xtn6shqgwbv324w4gdid.onion +wss://stonez.local:4848 +wss://notrustverify.ch +wss://woof.group +wss://mastodon.floe.earth +wss://lnbits.plebtag.com +wss://kpop.social +wss://relay.wavlake.com +wss://mastodon.sharma.io +wss://travelpandas.fr +wss://alphapanda.prowss +wss://relay.saes.io +wss://barelysocial.org +wss://masto.komintern.work +wss://norcal.social +wss://nostr.zbd.gg +wss://mk.outv.im +wss://mstdn.mx +wss://col.social +wss://nostr.freedom.fi +wss://filter.nostr.winebroadcasttrueglobalall +wss://eden.nostr.landv +wss://me.dm +wss://emacs.ch +wss://winonostr.wine +wss://infoplebstr.com +wss://mas.towss +wss://gruene.social +wss://relay.freeplace.nl +wss://itis.to +wss://bsky.social +wss://lnbits.thefockinfury.wtf +wss://5xxkt7zvmh4zdsjw64lgvchjdlrrgw4w2huujiiud35qms6gnkn5azad.onion +wss://eientei.org +wss://artisan.chat +wss://nustr.mom +wss://relay.nostrhraph.net +wss://shitposter.club +wss://nostril.cam +wss://nostr.spaceshell.xyz +wss://relay.wtr.app +wss://tdd.social +wss://d3meec25b53kegrnjmtmtyynikbkmuxf4jqgtk3sonjs6e62hpaezyqd.onion +wss://tkz.one +wss://freerelay.xyz +wss://nfdn.betanet.dotalgo.io +wss://mitra.social +wss://hablanews.io +wss://calle.wtf +wss://voskey.icalo.net +wss://nostr.sloyhy.win +wss://framapiaf.org +wss://nostrnodeofsven.com +wss://ciberlandia.pt +wss://gnusocial.net +wss://relay.s3x.socia +wss://paste.2nodez.com +wss://union.place +wss://bofh.socia +wss://sovbit.dev +wss://lnbits.btc-payserver.eu +wss://osna.social +wss://im-in.space +wss://junxingwang.org +wss://relay.nostr.wirednet.jpcheck +wss://libranet.de +wss://fedisnap.com +wss://woodpecker.social +wss://gensokyo.town +wss://social.imirhil.fr +wss://links.potsda.mn +wss://law-and-politics.online +wss://nostrich.bar +wss://tweesecake.social +wss://nostr.kisiel.net.pl +wss://relay.kisiel.net.pl +wss://calckey.social +wss://nostr.cercatrowa.me +wss://meganekeesu.tokyo +wss://lndiscs.duckdns.org +wss://omochi.xyz +wss://wue.social +wss://nostr.libreleaf.com +wss://rusnak.io +wss://rsslay-production.up.railway.app +wss://nostr.yuhr.org +wss://bod4ojj37fneith2setv3qjbii563wesbjqgdipdz4ag6voic2xk5iad.onion +wss://fediverse.blog +wss://pouet.chapril.org +wss://baq5ufl2rnczpalnoqabxwpjm3kvhzduwgvptxzx7yq37oqdbgf65syd.local +wss://baq5ufl2rnczpalnoqabxwpjm3kvhzduwgvptxzx7yq37oqdbgf65syd.onion +wss://cryptodon.lol +wss://nostr.debancariser.com +wss://mizunashi.hostdon.ne.jp +wss://relay.deezy.io +wss://bbq.snoot.com +wss://historians.social +wss://mi.mashiro.site +wss://mastodonpost.social +wss://nostrpub.welliorder.net +wss://social.cologne +wss://metapixl.com +wss://wandzeitung.xyz +wss://lightninhrelay.com +wss://techopolis.social +wss://lnbits.btcpins.com +wss://purplenostrich.com +wss://onewilshire.la +wss://sself.co +wss://anygemini13.blogs.sapo.pt +wss://federated.press +wss://metadata.nostr.com +wss://nostr.hodl.ar +wss://goreslut.xyz +wss://mastodon.com.tr +wss://climatejustice.global +wss://brotka.st +wss://sueden.social +wss://mstdn.fr +wss://abid.cc +wss://lnbits.fuckedbitcoin.com +wss://meow.social +wss://nostr.rehab +wss://mstdn.media +wss://nodeo1.nostress.cc +wss://nostrmassmux.com +wss://sauropods.win +wss://civilians.social +wss://pnw.zone +wss://zebeedee.cloud +wss://nostr.walletofsatishi.com +wss://aufovmqaxj5nhqmtorhgpogdjxefhkff25cbyyjt2sub3vwg6b6rplid.onion +wss://forfuture.social +wss://76f67qcwxsxpz7cfozlzunota2ejqznpldc5pnqtyq233hjpjzrmlfid.local +wss://toot.garden +wss://umbrel.tail9dfb.ts.net:4848 +wss://mastodon.chasem.dev +wss://misskey.pm +wss://merovingian.club +wss://chirp.enworld.org +wss://paid.no.str.ce +wss://masto.bike +wss://masto.1146.nohost.me +wss://eosla.comno-str.orgrelay.zeh.appno-str.orgrelay.zeh.app +wss://nixnet.social +wss://pay.zapit.live +wss://respublicae.eu +wss://nekomiya.net +wss://mastodon.internet-czas-dzialac.pl +wss://plushies.social +wss://lnb.openchain.fr +wss://mastodon.lol +wss://social.rebellion.global +wss://ruhr.social +wss://mi.farland.world +wss://pkutalk.com +wss://systemli.social +wss://nostr.minimue81.selfhost.com +wss://mastodon.la +wss://everything.happens.horse +wss://pagan.plus +wss://clacks.link +wss://u-tokyo.social +wss://fediverse.projectftm.com +wss://mastodon.bachgau.social +wss://social.kabi.tk +wss://ty3zdjkwlxo4zah6tgdoolznjcbvkhxpcvjyqe2buxeg23hbeyvr3rad.local +wss://pleroma.wakuwakup.net +wss://social.horrorhub.club +wss://nostr.nightowlstudios.ca +wss://mastodon.ml +wss://relay.orangepillapp.com +wss://misskey.sup39.dev +wss://mfmf.club +wss://pokemon.mastportal.info +wss://gohan-oisii.net +wss://aipi.social +wss://nostr.semisol.com +wss://oslo.town +wss://relay.layer.systems +wss://naharia.net +wss://social.elbespace.de +wss://linuxrocks.online +wss://b81m3pf94ridtry53g8ufyyrjtjaoxgbyjbs5k8qrqkr1whocxiy.loki:8080 +wss://lvl01.tater.ninja +wss://kinky.business +wss://relay.bitblockboom.com +wss://fedi.omada.cafe +wss://social.secret-wg.org +wss://celebrity.social +wss://weirdo.network +wss://mastodon.design +wss://berlin.social +wss://misskey.yukineko.me +wss://mindmachine.688.org +wss://sackheads.social +wss://a.farook.org +wss://social.ridetrans.it +wss://nostr-2.crypticthreadz.com +wss://test.itas.li +wss://fashionsocial.host +wss://ordinary.cafe +wss://social.arinbasu.online +wss://nostr.crypticthreadz.com +wss://relay.zebedee.cloud +wss://nostr.pub.wellorder.net +wss://09d4-5-161-189-144.ngrok-free.app +wss://andalucia.social +wss://udongein.xyz +wss://squeet.me +wss://mastodon.org.uk +wss://guild.pmdcollab.org +wss://relay.fi +wss://black.nostrscity.club +wss://relay.darker.to +wss://denostr.paiya.app +wss://noste.lu.ke +wss://wss.node01.nostress.cc +wss://theverge.space +wss://swiss.social +wss://relay.webstr.org +wss://nostr.shsbt.xyz +wss://relay.nostr.mom +wss://bitcoinmaximlaists.online +wss://relay.openhoofd.nl +wss://toot.aquilenet.fr +wss://toot.ale.gd +wss://relay.devstr.org +wss://lounge.town +wss://amala.schwartzwelt.xyz +wss://planetasieve.com.br +wss://alcrypt.ru:20911 +wss://webzero.grin.plus:8080 +wss://pylons.lightlns.com:28556 +wss://etourneau.fr:28343 +wss://sentie.relay.rts.network +wss://hermes.boarstudios.com +wss://non-central.pw +wss://nostrum.casa +wss://press.coop +wss://neovibe.app +wss://mstdn.starnix.network +wss://nostr.ameristraliagov.com +wss://cryptodon.chat +wss://umbrell.local:4848 +wss://mastodon.codingfield.com +wss://fe.disroot.org +wss://national.catposting.agency +wss://mastodon.pinewoodroad.net +wss://podcasts.social +wss://20nostr.semisol.dev +wss://nostr.oxtr.net +wss://mstdn.o-nature-culture.net +wss://dearcoati6.lnbits.com +wss://die-partei.social +wss://donotban.com +wss://creative.ai +wss://metaskey.net +wss://spacey.space +wss://node01.nostreess.cc +wss://node01.nostress.co +wss://niscii.xyz +wss://3gkpphcfwb6w5iq6axnmlbvr7pz2t37uy4ofocyijzttzrbz4jy43fid.onion +wss://3gkpphcfwb6w5iq6axnmlbvr7pz2t37uy4ofocyijzttzrbz4jy43fid.local +wss://sportsbots.xyz +wss://videos.lukesmith.xyz +wss://nostr.btcfreedom.ca +wss://gardenstate.social +wss://bg-btc.local:4848 +wss://0fa53e299287.ngrok.app +wss://bird.makeup +wss://nlayer.lbdev.fun +wss://relay.queiroz.vip +wss://mstdn.business +wss://osage.moe +wss://botrelay.com +wss://filter.wine +wss://gingadon.com +wss://noncentral.pw +wss://honi.club +wss://xn--baw-joa.social +wss://nostr.semisol.devwss +wss://relay.iris.to +wss://mynostrrelay.deno.dev +wss://social.heise.de +wss://vavursybkbgfyow7nnst5jnqsj2xyteusf3zeerbjdizq6y7h25v4syd.onion:5051 +wss://vavursybkbgfyow7nnst5jnqsj2xyteusf3zeerbjdizq6y7h25v4syd.onion:5050 +wss://relao.nostr.bg +wss://phpc.social +wss://mastodon.kylerank.in +wss://gameliberty.club +wss://rot.gives +wss://www.nostrweb.xyz +wss://ligma.pro +wss://mastodon.grin.hu +wss://geeknews.chat +wss://devdilettante.com +wss://relay.nosr-latam.link +wss://nosr.bitcoiner.social +wss://raru.re +wss://create-key.net +wss://lgbtqia.space +wss://fluffy.family +wss://mas.town +wss://bird.froth.zone +wss://akkoma.cryptoschizo.club +wss://relay.ramus.io +wss://40two.site +wss://relay.40two.site +wss://vis.social +wss://mk.paritybit.ca +wss://nyan.network +wss://ln.weedstr.net +wss://wikis.world +wss://social.fringe.com +wss://umbraxenu.no-ip.biz +wss://heads.social +wss://tsqdakwo4dh5ej3llsi52ftxfbialteu3jm4cmvxaksl3psbyeoyxxqd.onion +wss://jameliris.to +wss://mastodon.thirring.org +wss://miniwa.moe +wss://welcom.nostr.wine +wss://nostr.vulpem.comwss +wss://relay.semisol.dev +wss://kitsunes.club +wss://songbird.cloud +wss://nostr.wyssblitz.org +wss://libera.tokyo +wss://trpger.us +wss://comam.es +wss://nostr-pub.semisol.devaddittoyourrel +wss://social.dev-wiki.de +wss://ostfrie.se +wss://darmstadt.social +wss://nostr.cx.ms +wss://alentours.cc +wss://nostr.kleofash.eu +wss://gib.social +wss://test23.hifish.org +wss://rapemeat.solutions +wss://filter.stealth.winebroadcasttrue +wss://nostr.montre +wss://grumble.social +wss://nostr.roli.social +wss://primarycare.app +wss://hodlr.rocks +wss://superlinks.me +wss://mastodon.lawprofs.org +wss://tictoc.social +wss://nostest.dojotunnel.online +wss://kafka.icu +wss://nostr.lanparty.one +wss://filter.nostr.wineglobaltrue +wss://social.b10m.net +wss://worm.pink +wss://nostr-test.cx.ms +wss://nostrverifired.com +wss://relay.nostrss.re +wss://eliitin-some.fi +wss://dju.social +wss://jeremy.hu +wss://stream.criminallycute.fi +wss://nostr.0x50.dev +wss://n.s.nyc +wss://n.8.s.nyc +wss://relay.rocks +wss://relay.fiatjaf.com +wss://n-lan.s.nyc +wss://sciences.social +wss://nostr.swiss-enigma.com +wss://quietplace.xyz +wss://universe.nostrich.landlangenlangzh +wss://5280.city +wss://feddit.de +wss://base.lc +wss://social.medusmedia.com +wss://umha4zl6xk62a4dous6e7tq4qlmt462hlzs2su33en6qrvtvs3hkjgid.onion +wss://etorneau.fr:28343 +wss://foggyminds.com +wss://neurodiversity-in.au +wss://nostr.hendrixson.net +wss://ramen-fsm.eu.org +wss://www.nostrical.com +wss://feedbeat.me +wss://relay.bsky.social +wss://babka.social +wss://mastodontech.de +wss://commiespace.duckdns.org +wss://openbiblio.social +wss://karkatdyinginagluetrap.com +wss://relay.fundr.vanderwarker.family +wss://hannover.town +wss://nostr.relay.info +wss://mastodon.tetaneutral.net +wss://cache2.primal.net +wss://nostr.petrkr.net +wss://ifwo.eu +wss://mastodong.lol +wss://venera.social +wss://wallets.fyoumoneypod.com +wss://nostr.relay-nokotaro.com +wss://social.exozy.me +wss://gqgjp2bun4opme6mepz3rrgprkw4xatb6h5ogayorqwi6sajsxcp5sad.local +wss://branle.netlify.app +wss://rsslay.fiat.jaf +wss://chrislace.damus.io +wss://relay.leafbodhi.com +wss://social.opendesktop.org +wss://relay.nostr.watch +wss://gearlandia.haus +wss://freiburg.social +wss://atlas.nostro.land +wss://nostr.io +wss://patrizio.tn.al +wss://chaintools.io +wss://kn.icu +wss://relaywithme.eu +wss://la-autopilot-this-end-up.dvm.email +wss://coinfinity.co +wss://assemblag.es +wss://qou7zzll2mxx2ehl73n6pptmhizl5b3entowljlin3sqhcvltxdtlmad.onion +wss://indigenouscreatives.social +wss://bookwyrm.social +wss://kokoro.shugetsu.space +wss://eden.nost.land +wss://misskey.gothloli.club +wss://sersleepy.com +wss://rsslay.nos.pink +wss://pettingzoo.co +wss://witches.live +wss://7craxnzfi42touzi23etut5qjzqro27sqcuottxj7opntcin4fstruad.onion +wss://mastodonsweden.se +wss://mv2k.com +wss://nostr.tchaicap.space +wss://relay.whoop.ph +wss://bitcoiner.nostr.social +wss://kinkyelephant.com +wss://www.superstork.org +wss://mastodon.iftas.org +wss://lea.pet +wss://zug.network +wss://homeserver.local:4848 +wss://frontrange.co +wss://sciencemastodon.com +wss://montereybay.social +wss://social.securecryptomining.com +wss://rssrelay.nostr.moe +wss://nostr.org +wss://nostr.tw +wss://nostr.hk +wss://wxw.moe +wss://mastodonmusic.social +wss://puntarella.party +wss://oyasumi.space +wss://drumstodon.net +wss://social.wikimedia.de +wss://iyasaretai.pw +wss://social.coletivos.org +wss://nostream.localtest.me +wss://ubuntu201.local:4848 +wss://masto.pt +wss://taiwan.riley-tech.net +wss://freeradical.zone +wss://blastrf7z.xyz +wss://onemorestop.photo +wss://frikiverse.zone +wss://toot.bldrweb.org +wss://electroverse.tech +wss://mstdn.games +wss://relay1.nostr.unitedfop.com +wss://gratefuldread.masto.host +wss://mk.gabe.rocks +wss://widerweb.org +wss://mastodon.eternalaugust.com +wss://lay.southeastasia.cloudapp.azure.com:445 +wss://me.ns.ci +wss://gnostr.th +wss://fritter.cn +wss://nex.cn +wss://nex.tw +wss://fritter.jp +wss://fritter.tw +wss://gnostr.cn +wss://mstdn.dk +wss://akkoma.simulacrum-emporium.eu +wss://dalliance.social +wss://toot.re +wss://avatastic.uk +wss://blastr20f7z.xyz +wss://the.voiceover.bar +wss://porcodon.net +wss://nostr.dncn.xyz +wss://nostr.dnxn.xyz +wss://relay.xmr.rocks +wss://d6egak3woofrixu26gr3utb5qezhkktavsuwlrfqaauu55lmpudxudqd.onion:5051 +wss://social.wuebbsy.com +wss://4kgwkcfzea2xhefsquktyxqyjf3rsxa7oo7hxbcs6k3xdpxznknydsqd.local +wss://4kgwkcfzea2xhefsquktyxqyjf3rsxa7oo7hxbcs6k3xdpxznknydsqd.onion +wss://tooters.org +wss://nostre.wine +wss://relay.xplive.local +wss://relay2.xplive.local +wss://4v5umvicfs6a7d3aiy67uu2ibttiuanl2cehfmv5qaorbojousbgkdad.onion +wss://pipou.academy +wss://opjk6jxrcyicuwhe62tqy6zwx776u7rfi6cqo6iodurjvege7piz5wqd.local +wss://mythology.social +wss://nostr.millou.lol +wss://ryogrid.net:7777 +wss://nost.debancariser.com +wss://fault.stsecurity.moe +wss://jan-optiplex-5040.local:4848 +wss://relay-jp.wirednet.jp +wss://mastodon.kitchen +wss://relay.mnethome.de +wss://verkehrswende.social +wss://nostr.gleeze.com +wss://dair-community.social +wss://shota.house +wss://kavlak.uk +wss://social.inex.rocks +wss://4v5umvicfs6a7d3aiy67uu2ibttiuanl2cehfmv5qaorbojousbgkdad.local +wss://startrekshitposting.com +wss://nostrfmar.ddns.net +wss://4yqp7gzuf15zfc3hpwhz3j5p2uarvdsnf75ovpdiqvyjdsmku771jfid.onion +wss://nostrgraph.net +wss://idolheaven.org +wss://mstdn.kemono-friends.info +wss://mastodon.bawue.social +wss://social.pmj.rocks +wss://ursal.zone +wss://nstr.milou.lol +wss://poweredbygay.social +wss://gochisou.photo +wss://lnb3.openchain.fr +wss://nostr.filmweb.pl +wss://nostr-word.h3z.jp +wss://blastr.f7z.xyzanotherinstanceofblastr +wss://shakedown.social +wss://nostr.bubu.hair +wss://hyper-nostr.inosta.cc +wss://ieji.de +wss://wawmartme.com +wss://nostr.kungfu-g.rip +wss://bozgor.org +wss://todon.nl +wss://nostpy.lol +wss://ostatus.taiyolab.com +wss://polsum.rocks +wss://freespeech.group +wss://im.allmendenetz.de +wss://shitpost.poridge.club +wss://twingyeo.kr +wss://social.platypush.tech +wss://rsslay-production-bc22.up.railway.app +wss://lonely.damus.io +wss://kirche.social +wss://cubalibre.social +wss://relay.txinito.xyz +wss://realy.orangepill.dev +wss://3zi.ru +wss://plebstr.com +wss://social.seattle.wa.us +wss://social.bim.land +wss://cubhub.social +wss://relay.nostr-x.com +wss://hispagatos.space +wss://node101.nostress.cc +wss://lsbt.me +wss://jgqaglhautb4k6e6i2g34jakxiemqp6z4wynlirltuukgkft2xuglmqd.onion +wss://nostdemo.dojotunnel.online +wss://f.cz \ No newline at end of file From 84a52a1ce043a9b9d6f681bb0ca456fe13c5d7cd Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 20 Feb 2025 17:10:24 -0500 Subject: [PATCH 38/76] Speeding MurMur up Adding more test cases Mapping out memory used by filter --- .../quartz/benchmark/HintIndexerBenchmark.kt | 34 ++-- .../quartz/benchmark/MurMurBenchmark.kt | 46 +++++ .../quartz/nip01Core/hints/HintIndexer.kt | 82 +++++++-- .../nip01Core/hints/bloom/MurmurHash3.kt | 86 ++++----- .../quartz/nip01Core/hints/HintIndexerTest.kt | 163 +++++++++++++----- .../quartz/nip01Core/hints/MurMur3Test.kt | 76 ++++++++ .../vitorpamplona/quartz/utils/RuntimeExt.kt | 27 +++ 7 files changed, 388 insertions(+), 126 deletions(-) create mode 100644 benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/MurMurBenchmark.kt create mode 100644 quartz/src/test/java/com/vitorpamplona/quartz/nip01Core/hints/MurMur3Test.kt create mode 100644 quartz/src/test/java/com/vitorpamplona/quartz/utils/RuntimeExt.kt diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/HintIndexerBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/HintIndexerBenchmark.kt index 0baf72bc8f..3680267451 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/HintIndexerBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/HintIndexerBenchmark.kt @@ -24,6 +24,8 @@ import androidx.benchmark.junit4.BenchmarkRule import androidx.benchmark.junit4.measureRepeated import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.hints.HintIndexer import com.vitorpamplona.quartz.utils.RandomInstance import org.junit.Rule @@ -35,20 +37,22 @@ import java.nio.charset.Charset class HintIndexerBenchmark { @get:Rule val benchmarkRule = BenchmarkRule() - val keys = - mutableListOf().apply { - for (seed in 0..1_000_000) { - add(RandomInstance.bytes(32)) + companion object { + val keys = + mutableListOf().apply { + for (seed in 0..1_000_000) { + add(RandomInstance.bytes(32).toHexKey()) + } } - } - val relays = - getInstrumentation() - .context.assets - .open("relayDB.txt") - .readBytes() - .toString(Charset.forName("utf-8")) - .split("\n") + val relays = + getInstrumentation() + .context.assets + .open("relayDB.txt") + .readBytes() + .toString(Charset.forName("utf-8")) + .split("\n") + } @Test fun relayUriHashcode() { @@ -63,14 +67,14 @@ class HintIndexerBenchmark { keys.forEach { key -> (0..5).map { - indexer.index(key, relays.random()) + indexer.addKey(key, relays.random()) } } val key = keys.random() benchmarkRule.measureRepeated { - indexer.get(key) + indexer.getKey(key) } } @@ -80,7 +84,7 @@ class HintIndexerBenchmark { val indexer = HintIndexer() keys.forEach { key -> (0..5).map { - indexer.index(key, relays.random()) + indexer.addKey(key, relays.random()) } } } diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/MurMurBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/MurMurBenchmark.kt new file mode 100644 index 0000000000..7f79cd35d1 --- /dev/null +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/MurMurBenchmark.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.benchmark + +import androidx.benchmark.junit4.BenchmarkRule +import androidx.benchmark.junit4.measureRepeated +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.vitorpamplona.quartz.nip01Core.hints.bloom.MurmurHash3 +import com.vitorpamplona.quartz.utils.RandomInstance +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class MurMurBenchmark { + @get:Rule val benchmarkRule = BenchmarkRule() + + @Test + fun hash() { + val hasher = MurmurHash3() + + val byteArray = RandomInstance.bytes(32) + + benchmarkRule.measureRepeated { + hasher.hash(byteArray, 293) + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/HintIndexer.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/HintIndexer.kt index b4b41e8b05..cfc2b1df37 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/HintIndexer.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/HintIndexer.kt @@ -20,30 +20,84 @@ */ package com.vitorpamplona.quartz.nip01Core.hints +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray import com.vitorpamplona.quartz.nip01Core.hints.bloom.BloomFilterMurMur3 /** - * Instead of having one bloom filter per relay, which could create many - * large filters for very few events, this class uses only one mega bloom - * filter and uses the hashcode of the relay uri as differentiator in salt. + * Instead of having one bloom filter per relay per type, which could create + * many large bloom filters for collections of very few items, this class uses + * only one mega bloom filter per type and uses the hashcode of the relay uri + * as seed differentiator in the hash function. */ -class HintIndexer( - size: Int = 10_000_000, - rounds: Int = 5, -) { - private val bloomFilter = BloomFilterMurMur3(size, rounds) +class HintIndexer { + private val eventHints = BloomFilterMurMur3(10_000_000, 5) + private val addressHints = BloomFilterMurMur3(1_000_000, 5) + private val pubKeyHints = BloomFilterMurMur3(10_000_000, 5) private val relayDB = hashSetOf() - fun index( + private fun add( id: ByteArray, relay: String, + bloom: BloomFilterMurMur3, ) { relayDB.add(relay) - bloomFilter.add(id, relay.hashCode()) + bloom.add(id, relay.hashCode()) } - fun get(id: ByteArray): List = - relayDB.filter { - bloomFilter.mightContain(id, it.hashCode()) - } + private fun get( + id: ByteArray, + bloom: BloomFilterMurMur3, + ) = relayDB.filter { bloom.mightContain(id, it.hashCode()) } + + // -------------------- + // Event Host hints + // -------------------- + fun addEvent( + eventId: ByteArray, + relay: String, + ) = add(eventId, relay, eventHints) + + fun addEvent( + eventId: HexKey, + relay: String, + ) = addEvent(eventId.hexToByteArray(), relay) + + fun getEvent(eventId: ByteArray) = get(eventId, eventHints) + + fun getEvent(eventId: HexKey) = getEvent(eventId.hexToByteArray()) + + // -------------------- + // PubKeys Outbox hints + // -------------------- + fun addAddress( + addressId: ByteArray, + relay: String, + ) = add(addressId, relay, addressHints) + + fun addAddress( + addressId: String, + relay: String, + ) = addAddress(addressId.toByteArray(), relay) + + fun getAddress(addressId: ByteArray) = get(addressId, addressHints) + + fun getAddress(addressId: String) = getAddress(addressId.toByteArray()) + + // -------------------- + // PubKeys Outbox hints + // -------------------- + fun addKey( + key: ByteArray, + relay: String, + ) = add(key, relay, pubKeyHints) + + fun addKey( + key: HexKey, + relay: String, + ) = addKey(key.hexToByteArray(), relay) + + fun getKey(key: ByteArray) = get(key, pubKeyHints) + + fun getKey(key: HexKey) = getKey(key.hexToByteArray()) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/bloom/MurmurHash3.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/bloom/MurmurHash3.kt index 85c82db3f4..ca12d5aba6 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/bloom/MurmurHash3.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/bloom/MurmurHash3.kt @@ -20,61 +20,54 @@ */ package com.vitorpamplona.quartz.nip01Core.hints.bloom +import org.apache.commons.lang3.BooleanUtils.xor + class MurmurHash3 { - fun hash( - data: ByteArray, - seed: Int, - ) = hash(data, 0, data.size, seed) + companion object { + val ROUND_DOWN = 0xFFFFFFFC.toInt() + val C1 = -0x3361d2af // 0xcc9e2d51 + val C2 = 0x1b873593 + } /** * Generates 32 bit hash . * @param data the byte array to hash - * @param offset the start offset of the data in the array (always 0) - * @param length the length of the data in the array * @param seed the seed for the hash (int) * @return 32 bit hash of the given array */ fun hash( data: ByteArray, - offset: Int, - length: Int, seed: Int, ): Int { - val c1 = -0x3361d2af // 0xcc9e2d51 - val c2 = 0x1b873593 var h1 = seed - val roundedEnd = offset + (length and 0xFFFFFFFC.toInt()) // Round down to 4-byte blocks + val roundedEnd = data.size and ROUND_DOWN // Round down to 4-byte blocks - var i = offset + var i = 0 + var k1 = 0 while (i < roundedEnd) { - var k1 = - (data[i].toInt() and 0xFF) or - ((data[i + 1].toInt() and 0xFF) shl 8) or - ((data[i + 2].toInt() and 0xFF) shl 16) or - ((data[i + 3].toInt() and 0xFF) shl 24) + k1 = + ( + data[i++].toInt() and 0xFF or + (data[i++].toInt() and 0xFF shl 8) or + (data[i++].toInt() and 0xFF shl 16) or + (data[i++].toInt() and 0xFF shl 24) + ) * C1 - i += 4 - - k1 *= c1 - k1 = Integer.rotateLeft(k1, 15) - k1 *= c2 - - h1 = h1 xor k1 - h1 = Integer.rotateLeft(h1, 13) - h1 = h1 * 5 + -0x19ab949c // 0xe6546b64 + h1 = h1 xor (((k1 shl 15) or (k1 ushr -15)) * C2) + h1 = ((h1 shl 13) or (h1 ushr -13)) * 5 + -0x19ab949c // 0xe6546b64 } // processing tail (remaining bytes) - var k1 = 0 - when (length and 3) { + k1 = 0 + when (data.size and 3) { 3 -> { k1 = k1 or ((data[i + 2].toInt() and 0xFF) shl 16) k1 = k1 or ((data[i + 1].toInt() and 0xFF) shl 8) k1 = k1 or (data[i].toInt() and 0xFF) - k1 *= c1 - k1 = Integer.rotateLeft(k1, 15) - k1 *= c2 + k1 *= C1 + k1 = (k1 shl 15) or (k1 ushr -15) + k1 *= C2 h1 = h1 xor k1 } @@ -83,9 +76,9 @@ class MurmurHash3 { k1 = k1 or (data[i + 1].toInt() and 0xFF shl 8) k1 = k1 or (data[i].toInt() and 0xFF) - k1 *= c1 - k1 = Integer.rotateLeft(k1, 15) - k1 *= c2 + k1 *= C1 + k1 = (k1 shl 15) or (k1 ushr -15) + k1 *= C2 h1 = h1 xor k1 } @@ -93,29 +86,22 @@ class MurmurHash3 { 1 -> { k1 = k1 or (data[i].toInt() and 0xFF) - k1 *= c1 - k1 = Integer.rotateLeft(k1, 15) - k1 *= c2 + k1 *= C1 + k1 = (k1 shl 15) or (k1 ushr -15) + k1 *= C2 h1 = h1 xor k1 } } // final mix - h1 = h1 xor length - h1 = fmix32(h1) + h1 = h1 xor data.size + + // fmix32 + h1 = (h1 xor (h1 ushr 16)) * -0x7a143595 // 0x85ebca6b + h1 = (h1 xor (h1 ushr 13)) * -0x3d4d51cb // 0xc2b2ae35 + h1 = h1 xor (h1 ushr 16) return h1 } - - private fun fmix32(h: Int): Int { - var f = h - f = f xor (f ushr 16) - f *= -0x7a143595 // 0x85ebca6b - f = f xor (f ushr 13) - f *= -0x3d4d51cb // 0xc2b2ae35 - f = f xor (f ushr 16) - - return f - } } diff --git a/quartz/src/test/java/com/vitorpamplona/quartz/nip01Core/hints/HintIndexerTest.kt b/quartz/src/test/java/com/vitorpamplona/quartz/nip01Core/hints/HintIndexerTest.kt index 7bff250128..8676448ed2 100644 --- a/quartz/src/test/java/com/vitorpamplona/quartz/nip01Core/hints/HintIndexerTest.kt +++ b/quartz/src/test/java/com/vitorpamplona/quartz/nip01Core/hints/HintIndexerTest.kt @@ -21,73 +21,142 @@ package com.vitorpamplona.quartz.nip01Core.hints import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray import com.vitorpamplona.quartz.nip01Core.core.toHexKey -import com.vitorpamplona.quartz.nip01Core.hints.types.PubKeyHint +import com.vitorpamplona.quartz.nip01Core.hints.HintIndexerTest.Companion.indexer +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.utils.RandomInstance +import com.vitorpamplona.quartz.utils.usedMemoryMb import junit.framework.TestCase.assertTrue import org.junit.Test class HintIndexerTest { - val key1 = "ca29c211f1c72d5b6622268ff43d2288ea2b2cb5b9aa196ff9f1704fc914b71b".hexToByteArray() - val key2 = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c".hexToByteArray() - val key3 = "560c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c".hexToByteArray() + companion object { + val VALID_CHARS: List = ('0'..'9') + ('a'..'z') + ('A'..'Z') - val keys = - mutableListOf().apply { - for (seed in 0..1_000_000) { - add(RandomInstance.bytes(32).toHexKey()) + private fun randomChars(size: Int): String = CharArray(size) { VALID_CHARS.random() }.concatToString() + + val keys = + mutableListOf().apply { + for (seed in 0..1_000_000) { + add(RandomInstance.bytes(32).toHexKey()) + } } + + val eventIds = + mutableListOf().apply { + for (seed in 0..1_000_000) { + add(RandomInstance.bytes(32).toHexKey()) + } + } + + val addresses = + keys.take(10_000).map { + Address.assemble( + RandomInstance.int(150_000), + it, + randomChars(10), + ) + } + + val relays = + this::class.java + .getResourceAsStream("relayDB.txt") + ?.readAllBytes() + .toString() + .split("\n") + + val indexer by lazy { + System.gc() + Thread.sleep(1000) + + val startingMemory = Runtime.getRuntime().usedMemoryMb() + val result = HintIndexer() + val endingMemory = Runtime.getRuntime().usedMemoryMb() + + println("Filter using ${endingMemory - startingMemory}MB") + + // Simulates 5 outbox relays for each key + keys.forEach { key -> + (0..5).map { + result.addKey(key, relays.random()) + } + } + + // Simulates each event being in 8 + 0..10 relays. + eventIds.forEach { id -> + repeat(8 + RandomInstance.int(10)) { + result.addEvent(id, relays.random()) + } + } + + // Simulates each address being in 8 + 0..10 relays. + addresses.forEach { address -> + repeat(8 + RandomInstance.int(10)) { + result.addAddress(address, relays.random()) + } + } + + result } + } - val relays = - this.javaClass - .getResourceAsStream("relayDB.txt") - ?.readAllBytes() - .toString() - .split("\n") - - val keyHints = - keys - .map { key -> - (0..5).map { PubKeyHint(key, relays.random()) } - }.flatten() - - val key1Relays = (0..5).map { relays.random() } - val key2Relays = (0..4).map { relays.random() } - val key3Relays = (0..3).map { relays.random() } - - @Test - fun runExistingKeys() { - val indexer = HintIndexer() - keyHints.forEach { - indexer.index(it.id(), it.relay!!) - } + val testSize = 1000 + val testProb = 0.015f + fun assert99PercentSucess(success: () -> Boolean) { var failureCounter = 0 - repeat(1000) { - val key = keys.random() - if (indexer.get(key.hexToByteArray()).isEmpty()) { + repeat(testSize) { + if (!success()) { failureCounter++ } } - assertTrue("Failures $failureCounter ${failureCounter / 1_000.0}", failureCounter / 1_000.0f < 0.015) + assertTrue( + "Failure rate: $failureCounter of 1000 elements => ${(failureCounter / testSize.toFloat()) * 100}%", + failureCounter / testSize.toFloat() < testProb, + ) } @Test - fun runProb() { - val indexer = HintIndexer() - keyHints.forEach { - indexer.index(it.id(), it.relay!!) + fun runProbExistingKeys() = + assert99PercentSucess { + indexer.getKey(keys.random()).isNotEmpty() } - var failureCounter = 0 - repeat(1_000) { - if (indexer.get(RandomInstance.bytes(32)).isNotEmpty()) { - failureCounter++ - } + @Test + fun runProbNewKeys() = + assert99PercentSucess { + indexer.getKey(RandomInstance.bytes(32)).isEmpty() + } + + @Test + fun runProbExistingEventIds() = + assert99PercentSucess { + indexer.getEvent(eventIds.random()).isNotEmpty() + } + + @Test + fun runProbNewEventIds() = + assert99PercentSucess { + indexer.getEvent(RandomInstance.bytes(32)).isEmpty() + } + + @Test + fun runProbExistingAddresses() = + assert99PercentSucess { + indexer.getAddress(addresses.random()).isNotEmpty() + } + + @Test + fun runProbNewAddresses() = + assert99PercentSucess { + val newAddress = + Address.assemble( + RandomInstance.int(65000), + RandomInstance.bytes(32).toHexKey(), + randomChars(10), + ) + + indexer.getAddress(newAddress).isEmpty() } - assertTrue("Failures $failureCounter ${failureCounter / 1_000.0}", failureCounter / 1_000.0f < 0.015) - } } diff --git a/quartz/src/test/java/com/vitorpamplona/quartz/nip01Core/hints/MurMur3Test.kt b/quartz/src/test/java/com/vitorpamplona/quartz/nip01Core/hints/MurMur3Test.kt new file mode 100644 index 0000000000..195099cb41 --- /dev/null +++ b/quartz/src/test/java/com/vitorpamplona/quartz/nip01Core/hints/MurMur3Test.kt @@ -0,0 +1,76 @@ +/** + * 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.hints + +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray +import com.vitorpamplona.quartz.nip01Core.hints.bloom.MurmurHash3 +import junit.framework.TestCase.assertEquals +import org.junit.Test + +class MurMur3Test { + class Case( + val bytesHex: HexKey, + val seed: Int, + val result: Int, + ) + + val testCases = + listOf( + Case("9fd4e9a905ca9e1a3086fa4c0a1ed829dbf18c15ec05af95c76b78d3d2f5651b", 886838366, -525456393), + Case("e6c8f70f0d35a983bfebd00e5f29787c009c52971cfb4ac3a49b534b256b59cc", 1717487548, 1605080838), + Case("7f7113833feb31e877f193e2fc75a64e9c70252c3ae3c73373ff34430ae40ea6", 1275582690, 225480992), + Case("61770be6ec9df0f490743318e796e28ae34609732b61d365947871532d77d697", 514559346, 1424957638), + Case("375f46b4687ba3cd035db303fa294d943816e64ca6b3adcda2ae40e8ac9d91a0", 1898708424, 1730418066), + Case("c67044cd1d07a2aeb92b7bec973b6feb8abb9197840c59c101cacaa992489d49", 294602161, -1944496371), + Case("49db4bfcc4da62e38c4076843cdde1425570806f09f121f5e7f2507c5ee1db85", 910710684, 944243368), + Case("c5e98a30dead5ade4900b26eabae3435cfcdb64ff5e55c99641915a0c6ee73fc", 1107230285, 1550302684), + Case("b0ed2e7568e6b4e1d5e5bab46fde01149331b824e48a281798d7216dde8f5890", 1013875681, -1265544300), + Case("805f290e865bde094d77e82fb8b338d83347bc5449a4aed9fb08afb6a53a079b", 1674416787, -1821262025), + // special cases + Case("805f290e865bde094d77e82fb8b338d83347bc5449a4aed9fb08afb6a53a079b", Int.MAX_VALUE, -422576759), + Case("805f290e865bde094d77e82fb8b338d83347bc5449a4aed9fb08afb6a53a079b", Int.MAX_VALUE + 1, 851385048), + Case("805f290e865bde094d77e82fb8b338d83347bc5449a4aed9fb08afb6a53a079b", Int.MIN_VALUE, 851385048), + Case("805f290e865bde094d77e82fb8b338d83347bc5449a4aed9fb08afb6a53a079b", 0, 1615518380), + Case("fd", 1, 975430984), + Case("00", 1, 0), + Case("FF", 1, -797126820), + Case("3033", 1, 1435178296), + Case("0000", 1, -2047822809), + Case("FFFF", 1, 1459517456), + Case("3652a8", 1, 103723868), + Case("000000", 1, 821347078), + Case("FFFFFF", 1, -761438248), + Case("00000000", 1, 2028806445), + Case("FFFFFFFF", 1, 919009801), + ) + + @Test + fun testMurMur() { + val hasher = MurmurHash3() + testCases.forEach { + assertEquals( + it.result, + hasher.hash(it.bytesHex.hexToByteArray(), it.seed), + ) + } + } +} diff --git a/quartz/src/test/java/com/vitorpamplona/quartz/utils/RuntimeExt.kt b/quartz/src/test/java/com/vitorpamplona/quartz/utils/RuntimeExt.kt new file mode 100644 index 0000000000..f43acce2ec --- /dev/null +++ b/quartz/src/test/java/com/vitorpamplona/quartz/utils/RuntimeExt.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.utils + +fun Runtime.usedMemoryMb(): Long { + val totalMemoryMb = totalMemory() / (1024 * 1024) + val freeMemoryMb = freeMemory() / (1024 * 1024) + return totalMemoryMb - freeMemoryMb +} From 3220d793b404d00db13c4a5daac26716f75d332c Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 20 Feb 2025 17:44:36 -0500 Subject: [PATCH 39/76] Using a linked set for the relayDB is faster to loop through on mightContain --- .../com/vitorpamplona/quartz/nip01Core/hints/HintIndexer.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/HintIndexer.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/HintIndexer.kt index cfc2b1df37..c9659b45ea 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/HintIndexer.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/HintIndexer.kt @@ -34,7 +34,7 @@ class HintIndexer { private val eventHints = BloomFilterMurMur3(10_000_000, 5) private val addressHints = BloomFilterMurMur3(1_000_000, 5) private val pubKeyHints = BloomFilterMurMur3(10_000_000, 5) - private val relayDB = hashSetOf() + private val relayDB = mutableSetOf() private fun add( id: ByteArray, From dc4240a8f586a1160fd1ec4771d054f6c4693c12 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 20 Feb 2025 18:06:17 -0500 Subject: [PATCH 40/76] Increasing the amount of addresses --- .../com/vitorpamplona/quartz/nip01Core/hints/HintIndexer.kt | 2 +- .../vitorpamplona/quartz/nip01Core/hints/HintIndexerTest.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/HintIndexer.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/HintIndexer.kt index c9659b45ea..efee3a2be5 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/HintIndexer.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/HintIndexer.kt @@ -32,7 +32,7 @@ import com.vitorpamplona.quartz.nip01Core.hints.bloom.BloomFilterMurMur3 */ class HintIndexer { private val eventHints = BloomFilterMurMur3(10_000_000, 5) - private val addressHints = BloomFilterMurMur3(1_000_000, 5) + private val addressHints = BloomFilterMurMur3(2_000_000, 5) private val pubKeyHints = BloomFilterMurMur3(10_000_000, 5) private val relayDB = mutableSetOf() diff --git a/quartz/src/test/java/com/vitorpamplona/quartz/nip01Core/hints/HintIndexerTest.kt b/quartz/src/test/java/com/vitorpamplona/quartz/nip01Core/hints/HintIndexerTest.kt index 8676448ed2..4334b3465f 100644 --- a/quartz/src/test/java/com/vitorpamplona/quartz/nip01Core/hints/HintIndexerTest.kt +++ b/quartz/src/test/java/com/vitorpamplona/quartz/nip01Core/hints/HintIndexerTest.kt @@ -50,9 +50,9 @@ class HintIndexerTest { } val addresses = - keys.take(10_000).map { + keys.take(100_000).map { Address.assemble( - RandomInstance.int(150_000), + RandomInstance.int(65_000), it, randomChars(10), ) From 4b10dd58df60db142b3d82f36545afeadc0786af Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sat, 22 Feb 2025 16:17:10 -0500 Subject: [PATCH 41/76] It's slightly faster to reject params in one line than on many. --- quartz/src/main/java/com/vitorpamplona/quartz/utils/Hex.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/utils/Hex.kt b/quartz/src/main/java/com/vitorpamplona/quartz/utils/Hex.kt index 539a40ba6c..9aeb6aa410 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/utils/Hex.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/utils/Hex.kt @@ -38,9 +38,7 @@ object Hex { @JvmStatic fun isHex(hex: String?): Boolean { - if (hex == null) return false - if (hex.isEmpty()) return false - if (hex.length and 1 != 0) return false // must be even + if (hex.isNullOrEmpty() || hex.length and 1 != 0) return false try { for (c in hex.indices) { From 96f57b2c90e7d425b8699fde1d8948f4e30af749 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sat, 22 Feb 2025 16:18:46 -0500 Subject: [PATCH 42/76] Fixes tag name on ImageTag --- .../com/vitorpamplona/quartz/nip94FileMetadata/tags/ImageTag.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 69dfa8ded4..7aa7274a2c 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/ImageTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/ImageTag.kt @@ -33,7 +33,7 @@ class ImageTag( @JvmStatic fun parse(tag: Array): ImageTag? { - if (tag.size < TAG_SIZE || tag[0] != ThumbTag.TAG_NAME) return null + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null return ImageTag(tag[1], tag.getOrNull(2)) } From 3790325a0f6f83995033f66eef00283e2bd176d8 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sat, 22 Feb 2025 16:19:01 -0500 Subject: [PATCH 43/76] uses const val for faster MurMur --- .../quartz/nip01Core/hints/bloom/MurmurHash3.kt | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/bloom/MurmurHash3.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/bloom/MurmurHash3.kt index ca12d5aba6..280ef14391 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/bloom/MurmurHash3.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/bloom/MurmurHash3.kt @@ -20,13 +20,11 @@ */ package com.vitorpamplona.quartz.nip01Core.hints.bloom -import org.apache.commons.lang3.BooleanUtils.xor - class MurmurHash3 { companion object { - val ROUND_DOWN = 0xFFFFFFFC.toInt() - val C1 = -0x3361d2af // 0xcc9e2d51 - val C2 = 0x1b873593 + const val ROUND_DOWN = 0xFFFFFFFC.toInt() + const val C1 = -0x3361d2af // 0xcc9e2d51 + const val C2 = 0x1b873593 } /** From a2d9f31a8aa8490565ca83258f8bea9151ae1204 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 24 Feb 2025 14:57:15 -0500 Subject: [PATCH 44/76] Assigning hints to NIPs until NIP-53 Speeding up Bech32 and Hex processing Updating Boosts and Reposts to the new way of declaring tags Renaming the SHA256 hash tag from NIP94 to not confuse with # hashtags --- .../amethyst/ImageUploadTesting.kt | 2 +- .../vitorpamplona/amethyst/model/Account.kt | 4 +- .../amethyst/model/LocalCache.kt | 8 +- .../amethyst/ui/note/types/PeopleList.kt | 3 +- .../ui/screen/AccountStateViewModel.kt | 2 +- .../quartz/benchmark/EnsureTest.kt | 143 ++++++++++++++++++ .../quartz/benchmark/HexBenchmark.kt | 15 ++ .../commons/richtext/RichTextParser.kt | 6 +- .../nip95/header/FileStorageHeaderEvent.kt | 4 +- .../nip95/header/TagArrayBuilderExt.kt | 4 +- .../ProfileGalleryEntryEvent.kt | 4 +- .../profileGallery/TagArrayBuilderExt.kt | 4 +- .../RelationshipStatusEvent.kt | 4 +- .../quartz/nip01Core/core/HexKey.kt | 4 + .../quartz/nip01Core/core/Tag.kt | 10 ++ .../{HintProvider.kt => HintProviders.kt} | 16 +- .../nip01Core/tags/addressables/ATag.kt | 7 + .../quartz/nip01Core/tags/events/ETag.kt | 13 +- .../hashtags/{HashTag.kt => HashtagTag.kt} | 4 +- .../tags/hashtags/TagArrayBuilderExt.kt | 4 +- .../nip01Core/tags/hashtags/TagArrayExt.kt | 16 +- .../quartz/nip01Core/tags/people/PTag.kt | 12 +- .../nip02FollowList/ContactListEvent.kt | 17 ++- .../nip02FollowList/tags/AddressFollowTag.kt | 25 +++ .../nip02FollowList/{ => tags}/ContactTag.kt | 21 ++- .../quartz/nip03Timestamp/OtsEvent.kt | 18 ++- .../nip03Timestamp/TagArrayBuilderExt.kt | 36 +++++ .../nip03Timestamp/tags/TargetEventKindTag.kt | 25 +++ .../nip03Timestamp/tags/TargetEventTag.kt | 25 +++ .../quartz/nip09Deletions/DeletionEvent.kt | 10 +- .../quartz/nip10Notes/BaseThreadedEvent.kt | 17 ++- .../quartz/nip10Notes/tags/MarkedETag.kt | 7 + .../quartz/nip17Dm/base/BaseDMGroupEvent.kt | 6 +- .../ChatMessageEncryptedFileHeaderEvent.kt | 4 +- .../nip17Dm/files/TagArrayBuilderExt.kt | 4 +- .../nip17Dm/files/tags/EncryptionNonce.kt | 2 +- .../quartz/nip18Reposts/GenericRepostEvent.kt | 29 +++- .../quartz/nip18Reposts/RepostEvent.kt | 31 +++- .../quartz/nip18Reposts/quotes/QTag.kt | 15 ++ .../quartz/nip19Bech32/bech32/Bech32Util.kt | 37 +++-- .../quartz/nip22Comments/CommentEvent.kt | 14 +- .../nip22Comments/tags/ReplyAddressTag.kt | 7 + .../nip22Comments/tags/ReplyAuthorTag.kt | 7 + .../nip22Comments/tags/ReplyEventTag.kt | 7 + .../nip22Comments/tags/RootAddressTag.kt | 7 + .../nip22Comments/tags/RootAuthorTag.kt | 37 ++++- .../quartz/nip22Comments/tags/RootEventTag.kt | 7 + .../nip23LongContent/LongTextNoteEvent.kt | 16 +- .../quartz/nip25Reactions/ReactionEvent.kt | 15 +- .../admin/ChannelCreateEvent.kt | 7 +- .../admin/ChannelHideMessageEvent.kt | 6 +- .../admin/ChannelMetadataEvent.kt | 7 +- .../admin/ChannelMuteUserEvent.kt | 6 +- .../selection/EmojiPackSelectionEvent.kt | 7 +- .../quartz/nip34Git/issue/GitIssueEvent.kt | 15 +- .../quartz/nip34Git/patch/GitPatchEvent.kt | 16 +- .../quartz/nip34Git/reply/GitReplyEvent.kt | 16 +- .../nip35Torrents/TorrentCommentEvent.kt | 16 +- .../quartz/nip51Lists/GeneralListEvent.kt | 37 +++-- .../quartz/nip51Lists/MuteListEvent.kt | 32 +--- .../quartz/nip51Lists/PeopleListEvent.kt | 65 +------- .../quartz/nip51Lists/PrivateTagArrayEvent.kt | 19 +-- .../PrivateTagsInContent.kt} | 4 +- .../quartz/nip51Lists/tags/NameTag.kt | 36 +++++ .../quartz/nip51Lists/tags/TitleTag.kt | 37 +++++ .../chat/LiveActivitiesChatMessageEvent.kt | 17 ++- .../quartz/nip68Picture/IMetaTagBuilderExt.kt | 4 +- .../quartz/nip68Picture/IMetaTagExt.kt | 4 +- .../quartz/nip68Picture/PictureEvent.kt | 4 +- .../quartz/nip68Picture/TagArrayBuilderExt.kt | 4 +- .../quartz/nip71Video/IMetaTagBuilderExt.kt | 4 +- .../quartz/nip71Video/IMetaTagExt.kt | 4 +- .../quartz/nip71Video/VideoEvent.kt | 4 +- .../nip94FileMetadata/FileHeaderEvent.kt | 4 +- .../nip94FileMetadata/IMetaTagBuilderExt.kt | 4 +- .../nip94FileMetadata/TagArrayBuilderExt.kt | 4 +- .../tags/{HashTag.kt => HashSha256Tag.kt} | 4 +- .../com/vitorpamplona/quartz/utils/Ensure.kt | 35 +++++ .../com/vitorpamplona/quartz/utils/Hex.kt | 13 +- 79 files changed, 916 insertions(+), 254 deletions(-) create mode 100644 benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/EnsureTest.kt rename quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/{HintProvider.kt => HintProviders.kt} (86%) rename quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/hashtags/{HashTag.kt => HashtagTag.kt} (95%) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/tags/AddressFollowTag.kt rename quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/{ => tags}/ContactTag.kt (80%) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/TagArrayBuilderExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/tags/TargetEventKindTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/tags/TargetEventTag.kt rename quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/{Nip51PrivateTags.kt => encryption/PrivateTagsInContent.kt} (96%) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/tags/NameTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/tags/TitleTag.kt rename quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/{HashTag.kt => HashSha256Tag.kt} (92%) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/utils/Ensure.kt diff --git a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ImageUploadTesting.kt b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ImageUploadTesting.kt index 138a46c337..bf76e2ab60 100644 --- a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ImageUploadTesting.kt +++ b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ImageUploadTesting.kt @@ -36,7 +36,7 @@ import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair -import com.vitorpamplona.quartz.utils.sha256 +import com.vitorpamplona.quartz.utils.sha256.sha256 import junit.framework.TestCase.assertEquals import junit.framework.TestCase.fail import kotlinx.coroutines.CoroutineScope 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 b1593b09d9..778f85b9a0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -99,8 +99,8 @@ import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser 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.nip02FollowList.tags.ContactTag import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent import com.vitorpamplona.quartz.nip04Dm.messages.reply @@ -3542,7 +3542,7 @@ class Account( val event = (addressableNote.event as? PeopleListEvent) event != null && event.pubKey == pubkey && - (event.hasAnyTaggedUser() || event.publicAndPrivateUserCache?.isNotEmpty() == true) + (event.hasAnyTaggedUser() || event.cachedPrivateTags()?.isNotEmpty() == true) } fun markAsRead( 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 e29ec5337b..3abeb310d1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -683,11 +683,11 @@ object LocalCache { is BadgeAwardEvent -> event.awardDefinition().map { getOrCreateAddressableNote(it) } is PrivateDmEvent -> event.taggedEvents().mapNotNull { checkGetOrCreateNote(it) } is RepostEvent -> - event.boostedPost().mapNotNull { checkGetOrCreateNote(it) } + - event.taggedAddresses().map { getOrCreateAddressableNote(it) } + event.boostedEventIds().mapNotNull { checkGetOrCreateNote(it) } + + event.boostedAddresses().map { getOrCreateAddressableNote(it) } is GenericRepostEvent -> - event.boostedPost().mapNotNull { checkGetOrCreateNote(it) } + - event.taggedAddresses().map { getOrCreateAddressableNote(it) } + event.boostedEventIds().mapNotNull { checkGetOrCreateNote(it) } + + event.boostedAddresses().map { getOrCreateAddressableNote(it) } is CommunityPostApprovalEvent -> event.approvedEvents().mapNotNull { checkGetOrCreateNote(it) } + event.approvedAddresses().map { getOrCreateAddressableNote(it) } 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 0da9a10ac7..4b47074ed6 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 @@ -54,6 +54,7 @@ import com.vitorpamplona.amethyst.ui.note.UserCompose import com.vitorpamplona.amethyst.ui.note.getGradient import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.DividerThickness +import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUserIds import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -94,7 +95,7 @@ fun DisplayPeopleList( ) LaunchedEffect(Unit) { - accountViewModel.loadUsers(noteEvent.bookmarkedPeopleIds()) { + accountViewModel.loadUsers(noteEvent.taggedUserIds()) { members = it } } 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 4e796e5df3..5763ea36f3 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 @@ -42,8 +42,8 @@ import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent -import com.vitorpamplona.quartz.nip02FollowList.ContactTag import com.vitorpamplona.quartz.nip02FollowList.ReadWrite +import com.vitorpamplona.quartz.nip02FollowList.tags.ContactTag import com.vitorpamplona.quartz.nip06KeyDerivation.Nip06 import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/EnsureTest.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/EnsureTest.kt new file mode 100644 index 0000000000..00ca68592a --- /dev/null +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/EnsureTest.kt @@ -0,0 +1,143 @@ +/** + * 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.benchmark + +import androidx.benchmark.junit4.BenchmarkRule +import androidx.benchmark.junit4.measureRepeated +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.vitorpamplona.quartz.utils.ensure +import junit.framework.TestCase.assertTrue +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class EnsureTest { + @get:Rule + val scope = BenchmarkRule() + + private val testArgs = arrayOf("0", "1f") + + companion object { + const val TAG = "0" + const val KEY_SIZE = 2 + + private val isHexChar = + BooleanArray(256).apply { + "0123456789abcdefABCDEF".forEach { this[it.code] = true } + } + + @JvmStatic + fun String.isHex() = all { isHexChar[it.code] } + } + + fun directCheckElse(args: Array) = + if (args[0] != TAG || args[1].length != KEY_SIZE || !args[1].isHex()) { + null + } else { + args[0] + args[1] + } + + fun directCheck(args: Array): String? { + if (args[0] != TAG || args[1].length != KEY_SIZE || !args[1].isHex()) return null + return args[0] + args[1] + } + + fun directCheck2(args: Array): String? { + if (args[0] == TAG && args[1].length == KEY_SIZE && args[1].isHex()) return args[0] + args[1] + return null + } + + fun linedCheck(args: Array): String? { + if (args[0] != TAG) return null + if (args[1].length != KEY_SIZE) return null + if (!args[1].isHex()) return null + return args[0] + args[1] + } + + fun ensureCheck(args: Array): String? { + ensure(args[0] == TAG) { return null } + ensure(args[1].length == KEY_SIZE) { return null } + ensure(args[1].isHex()) { return null } + return args[0] + args[1] + } + + fun checkCatch(args: Array): String? = + runCatching { + check(args[0] == TAG) + check(args[1].length == KEY_SIZE) + check(args[1].isHex()) + args[0] + args[1] + }.getOrNull() + + fun whenCheck(args: Array) = + when (false) { + (args[0] == TAG), + (args[1].length == KEY_SIZE), + (args[1].isHex()), + -> null + else -> args[0] + args[1] + } + + fun ensureAll(args: Array): String? { + ensureAll { + ensure { args[0] == TAG } + ensure { args[1].length == KEY_SIZE } + ensure { args[1].isHex() } + }.ifFail { return null } + return args[0] + args[1] + } + + @Test + fun directCheckElse() = scope.measureRepeated { assertTrue(directCheckElse(testArgs) != null) } + + @Test + fun directCheck() = scope.measureRepeated { assertTrue(directCheck(testArgs) != null) } + + @Test + fun linedCheck() = scope.measureRepeated { assertTrue(linedCheck(testArgs) != null) } + + @Test + fun ensureCheck() = scope.measureRepeated { assertTrue(ensureCheck(testArgs) != null) } + + @Test + fun checkCatch() = scope.measureRepeated { assertTrue(checkCatch(testArgs) != null) } + + @Test + fun whenCheck() = scope.measureRepeated { assertTrue(whenCheck(testArgs) != null) } + + @Test + fun ensureAll() = scope.measureRepeated { assertTrue(ensureAll(testArgs) != null) } +} + +class EnsureScope( + var passing: Boolean = true, +) { + inline fun ensure(predicate: () -> Boolean) { + if (passing) passing = predicate() + } + + inline fun ifFail(ifFail: () -> Unit) { + if (!passing) ifFail() + } +} + +inline fun ensureAll(block: EnsureScope.() -> Unit) = EnsureScope().apply(block) diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/HexBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/HexBenchmark.kt index b18ca104f3..beb5573725 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/HexBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/HexBenchmark.kt @@ -91,4 +91,19 @@ class HexBenchmark { fun isHex() { r.measureRepeated { Hex.isHex(hex) } } + + @Test + fun newIsHex() { + val isHexChar = + BooleanArray(256).apply { + "0123456789abcdefABCDEF".forEach { this[it.code] = true } + } + + r.measureRepeated { + for (c in hex.indices) { + if (!isHexChar[hex[c].code]) return@measureRepeated + } + return@measureRepeated + } + } } 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 a3325657ba..ee473f7fa0 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 @@ -33,7 +33,7 @@ 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.HashSha256Tag import com.vitorpamplona.quartz.nip94FileMetadata.tags.MimeTypeTag import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -79,7 +79,7 @@ class RichTextParser { MediaUrlImage( url = fullUrl, description = description ?: frags[AltTag.TAG_NAME] ?: tags[AltTag.TAG_NAME]?.firstOrNull(), - hash = frags[HashTag.TAG_NAME] ?: tags[HashTag.TAG_NAME]?.firstOrNull(), + hash = frags[HashSha256Tag.TAG_NAME] ?: tags[HashSha256Tag.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(), @@ -90,7 +90,7 @@ class RichTextParser { MediaUrlVideo( url = fullUrl, description = description ?: frags[AltTag.TAG_NAME] ?: tags[AltTag.TAG_NAME]?.firstOrNull(), - hash = frags[HashTag.TAG_NAME] ?: tags[HashTag.TAG_NAME]?.firstOrNull(), + hash = frags[HashSha256Tag.TAG_NAME] ?: tags[HashSha256Tag.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(), 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 index 621c9f852f..0642578926 100644 --- 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 @@ -34,7 +34,7 @@ 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.HashSha256Tag import com.vitorpamplona.quartz.nip94FileMetadata.tags.ImageTag import com.vitorpamplona.quartz.nip94FileMetadata.tags.MagnetTag import com.vitorpamplona.quartz.nip94FileMetadata.tags.MimeTypeTag @@ -62,7 +62,7 @@ class FileStorageHeaderEvent( fun mimeType() = tags.firstNotNullOfOrNull(MimeTypeTag::parse) - fun hash() = tags.firstNotNullOfOrNull(HashTag::parse) + fun hash() = tags.firstNotNullOfOrNull(HashSha256Tag::parse) fun size() = tags.firstNotNullOfOrNull(SizeTag::parse) 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 index 5e0d11845a..f4ec26cbee 100644 --- 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 @@ -25,7 +25,7 @@ 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.HashSha256Tag import com.vitorpamplona.quartz.nip94FileMetadata.tags.ImageTag import com.vitorpamplona.quartz.nip94FileMetadata.tags.MagnetTag import com.vitorpamplona.quartz.nip94FileMetadata.tags.MimeTypeTag @@ -37,7 +37,7 @@ 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.hash(hash: HexKey) = add(HashSha256Tag.assemble(hash)) fun TagArrayBuilder.fileSize(size: Int) = add(SizeTag.assemble(size)) 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 2d67776046..124b4fd9b1 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 @@ -31,7 +31,7 @@ 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.HashSha256Tag import com.vitorpamplona.quartz.nip94FileMetadata.tags.ImageTag import com.vitorpamplona.quartz.nip94FileMetadata.tags.MagnetTag import com.vitorpamplona.quartz.nip94FileMetadata.tags.MimeTypeTag @@ -58,7 +58,7 @@ class ProfileGalleryEntryEvent( fun mimeType() = tags.firstNotNullOfOrNull(MimeTypeTag::parse) - fun hash() = tags.firstNotNullOfOrNull(HashTag::parse) + fun hash() = tags.firstNotNullOfOrNull(HashSha256Tag::parse) fun size() = tags.firstNotNullOfOrNull(SizeTag::parse) 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 index 3a0d22a4a5..afa46548b9 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/profileGallery/TagArrayBuilderExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/profileGallery/TagArrayBuilderExt.kt @@ -26,7 +26,7 @@ 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.HashSha256Tag import com.vitorpamplona.quartz.nip94FileMetadata.tags.ImageTag import com.vitorpamplona.quartz.nip94FileMetadata.tags.MagnetTag import com.vitorpamplona.quartz.nip94FileMetadata.tags.MimeTypeTag @@ -42,7 +42,7 @@ fun TagArrayBuilder.url(url: String) = add(UrlTag.asse fun TagArrayBuilder.mimeType(mimeType: String) = add(MimeTypeTag.assemble(mimeType)) -fun TagArrayBuilder.hash(hash: HexKey) = add(HashTag.assemble(hash)) +fun TagArrayBuilder.hash(hash: HexKey) = add(HashSha256Tag.assemble(hash)) fun TagArrayBuilder.fileSize(size: Int) = add(SizeTag.assemble(size)) 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 46fe9fe74b..23792c2cbd 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 @@ -30,8 +30,8 @@ import com.vitorpamplona.quartz.nip01Core.core.tagArray import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner 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.nip51Lists.encryption.PrivateTagsInContent import com.vitorpamplona.quartz.utils.TimeUtils @Immutable @@ -77,7 +77,7 @@ class RelationshipStatusEvent( privateInitializer() } - Nip51PrivateTags.encryptNip44(privateTags, signer) { content -> + PrivateTagsInContent.encryptNip44(privateTags, signer) { content -> signer.sign(createdAt, KIND, publicTags, content, onReady) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/HexKey.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/HexKey.kt index caa4536f60..1330757784 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/HexKey.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/HexKey.kt @@ -28,3 +28,7 @@ typealias HexKey = String fun ByteArray.toHexKey(): HexKey = Hex.encode(this) fun HexKey.hexToByteArray(): ByteArray = Hex.decode(this) + +const val PUBKEY_LENGTH = 64 + +const val EVENT_ID_LENGTH = 64 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 index 71e22c6571..d97acc9c1c 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/Tag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/Tag.kt @@ -22,6 +22,16 @@ package com.vitorpamplona.quartz.nip01Core.core typealias Tag = Array +/** + * Returns if the Tag has at least $index elements. + */ +fun Tag.has(index: Int) = size > index + +/** + * Returns if the Tag has less than $index elements. + */ +fun Tag.lacks(index: Int) = size <= index + fun Tag.name() = this[0] fun Tag.value() = this[1] diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/HintProvider.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/HintProviders.kt similarity index 86% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/HintProvider.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/HintProviders.kt index 80e9521893..aa94e3ca5c 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/HintProvider.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/HintProviders.kt @@ -24,10 +24,14 @@ import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint import com.vitorpamplona.quartz.nip01Core.hints.types.PubKeyHint -interface HintProvider { - fun eventHints(): EventIdHint - - fun addressIdHints(): AddressHint - - fun pubKeyHints(): PubKeyHint +interface EventHintProvider { + fun eventHints(): List +} + +interface AddressHintProvider { + fun addressHints(): List +} + +interface PubKeyHintProvider { + fun pubKeyHints(): List } 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 36eba88da6..b174d3ff8c 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 @@ -25,6 +25,7 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey 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.nip01Core.hints.types.AddressHint import com.vitorpamplona.quartz.utils.arrayOfNotNull import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.pointerSizeInBytes @@ -128,6 +129,12 @@ data class ATag( return tag[1] } + @JvmStatic + fun parseAsHint(tag: Array): AddressHint? { + if (tag.size < 3 || tag[0] != TAG_NAME || !tag[1].contains(':') || tag[2].isEmpty()) return null + return AddressHint(tag[1], tag[2]) + } + @JvmStatic fun assemble( aTagId: HexKey, 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 d4d58791c0..0f59c78978 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 @@ -22,6 +22,7 @@ package com.vitorpamplona.quartz.nip01Core.tags.events import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent import com.vitorpamplona.quartz.utils.arrayOfNotNull import com.vitorpamplona.quartz.utils.bytesUsedInMemory @@ -58,7 +59,7 @@ data class ETag( const val TAG_SIZE = 2 @JvmStatic - fun isTagged(tag: Array) = tag.size >= TAG_SIZE && tag[0] == TAG_NAME && tag[1].isNotEmpty() + fun isTagged(tag: Array) = tag.size >= TAG_SIZE && tag[0] == TAG_NAME && tag[1].length == 64 @JvmStatic fun isTagged( @@ -68,16 +69,22 @@ data class ETag( @JvmStatic fun parse(tag: Array): ETag? { - if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME || tag[1].length != 64) 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 + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME || tag[1].length != 64) return null return tag[1] } + @JvmStatic + fun parseAsHint(tag: Array): EventIdHint? { + if (tag.size < 3 || tag[0] != TAG_NAME || tag[1].length != 64 || tag[2].isEmpty()) return null + return EventIdHint(tag[1], tag[2]) + } + @JvmStatic fun assemble( eventId: HexKey, 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/HashtagTag.kt similarity index 95% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/hashtags/HashTag.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/hashtags/HashtagTag.kt index 68e46b97ee..773b14db27 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/hashtags/HashTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/hashtags/HashtagTag.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.quartz.nip01Core.tags.hashtags -class HashTag { +class HashtagTag { companion object { const val TAG_NAME = "t" const val TAG_SIZE = 2 @@ -30,7 +30,7 @@ class HashTag { @JvmStatic fun parse(tag: Array): String? { - if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME || tag[1].isEmpty()) return null return tag[1] } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/hashtags/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/hashtags/TagArrayBuilderExt.kt index ec68e5877e..a6f7cee6de 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/hashtags/TagArrayBuilderExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/hashtags/TagArrayBuilderExt.kt @@ -23,6 +23,6 @@ package com.vitorpamplona.quartz.nip01Core.tags.hashtags import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder -fun TagArrayBuilder.hashtag(tag: String) = addAll(HashTag.assembleDualCase(tag)) +fun TagArrayBuilder.hashtag(tag: String) = addAll(HashtagTag.assembleDualCase(tag)) -fun TagArrayBuilder.hashtags(tag: List) = addAll(HashTag.assemble(tag)) +fun TagArrayBuilder.hashtags(tag: List) = addAll(HashtagTag.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 01466ff1b1..849e381672 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 @@ -30,18 +30,18 @@ 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(HashTag.TAG_NAME, onEach) +fun TagArray.forEachHashTag(onEach: (eventId: HexKey) -> Unit) = this.forEachTagged(HashtagTag.TAG_NAME, onEach) -fun TagArray.anyHashTag(onEach: (str: String) -> Boolean) = this.anyTagged(HashTag.TAG_NAME, onEach) +fun TagArray.anyHashTag(onEach: (str: String) -> Boolean) = this.anyTagged(HashtagTag.TAG_NAME, onEach) -fun TagArray.hasHashtags() = this.hasTagWithContent(HashTag.TAG_NAME) +fun TagArray.hasHashtags() = this.hasTagWithContent(HashtagTag.TAG_NAME) -fun TagArray.hashtags() = this.mapValues(HashTag.TAG_NAME) +fun TagArray.hashtags() = this.mapValues(HashtagTag.TAG_NAME) -fun TagArray.countHashtags() = this.count(HashTag::isTagged) +fun TagArray.countHashtags() = this.count(HashtagTag::isTagged) -fun TagArray.isTaggedHash(hashtag: String) = this.isTagged(HashTag.TAG_NAME, hashtag, true) +fun TagArray.isTaggedHash(hashtag: String) = this.isTagged(HashtagTag.TAG_NAME, hashtag, true) -fun TagArray.isTaggedHashes(hashtags: Set) = this.isAnyLowercaseTagged(HashTag.TAG_NAME, hashtags) +fun TagArray.isTaggedHashes(hashtags: Set) = this.isAnyLowercaseTagged(HashtagTag.TAG_NAME, hashtags) -fun TagArray.firstIsTaggedHashes(hashtags: Set) = this.firstAnyLowercaseTaggedValue(HashTag.TAG_NAME, hashtags) +fun TagArray.firstIsTaggedHashes(hashtags: Set) = this.firstAnyLowercaseTaggedValue(HashtagTag.TAG_NAME, hashtags) 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 index 9064ff6b0d..55a2270ac7 100644 --- 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 @@ -25,7 +25,7 @@ import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Tag import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray -import com.vitorpamplona.quartz.nip01Core.core.isNotName +import com.vitorpamplona.quartz.nip01Core.hints.types.PubKeyHint import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile import com.vitorpamplona.quartz.nip19Bech32.toNpub import com.vitorpamplona.quartz.utils.arrayOfNotNull @@ -59,14 +59,14 @@ data class PTag( @JvmStatic fun parse(tag: Tag): PTag? { - if (tag.isNotName(TAG_NAME, TAG_SIZE)) return null + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) 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.size < TAG_SIZE || tag[0] != TAG_NAME) return null if (tag[1].length != 64) { Log.w("PTag", "Invalid `$TAG_NAME` value ${tag.joinToString(", ")}") return null @@ -74,6 +74,12 @@ data class PTag( return tag[1] } + @JvmStatic + fun parseAsHint(tag: Array): PubKeyHint? { + if (tag.size < 3 || tag[0] != TAG_NAME || tag[1].length != 64 || tag[2].isEmpty()) return null + return PubKeyHint(tag[1], tag[2]) + } + @JvmStatic fun assemble( pubkey: HexKey, 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 8669f730e2..a5d995afbc 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/ContactListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/ContactListEvent.kt @@ -23,6 +23,8 @@ package com.vitorpamplona.quartz.nip02FollowList import androidx.compose.runtime.Stable import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag @@ -32,8 +34,9 @@ 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.nip02FollowList.tags.AddressFollowTag +import com.vitorpamplona.quartz.nip02FollowList.tags.ContactTag import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.utils.TimeUtils @@ -45,7 +48,13 @@ class ContactListEvent( tags: Array>, content: String, sig: HexKey, -) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { +) : Event(id, pubKey, createdAt, KIND, tags, content, sig), + AddressHintProvider, + PubKeyHintProvider { + override fun addressHints() = tags.mapNotNull(ATag::parseAsHint) + + override fun pubKeyHints() = tags.mapNotNull(ContactTag::parseAsHint) + /** * Returns a list of p-tags that are verified as hex keys. */ @@ -54,9 +63,9 @@ class ContactListEvent( /** * Returns a list of a-tags that are verified as correct. */ - fun verifiedFollowAddressSet(): Set = tags.mapNotNullTo(HashSet(), ATag::parseValidAddress) + fun verifiedFollowAddressSet(): Set = tags.mapNotNullTo(HashSet(), AddressFollowTag::parseValidAddress) - fun unverifiedFollowKeySet() = tags.mapNotNull(PTag::parseKey) + fun unverifiedFollowKeySet() = tags.mapNotNull(ContactTag::parseKey) fun unverifiedFollowTagSet() = tags.hashtags() diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/tags/AddressFollowTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/tags/AddressFollowTag.kt new file mode 100644 index 0000000000..cfe10b3a0c --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/tags/AddressFollowTag.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.nip02FollowList.tags + +import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag + +typealias AddressFollowTag = ATag diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/ContactTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/tags/ContactTag.kt similarity index 80% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/ContactTag.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/tags/ContactTag.kt index d3c26d2ee8..eab66cef33 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/ContactTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/tags/ContactTag.kt @@ -18,12 +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.nip02FollowList +package com.vitorpamplona.quartz.nip02FollowList.tags import android.util.Log import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.toHexKey +import com.vitorpamplona.quartz.nip01Core.hints.types.PubKeyHint import com.vitorpamplona.quartz.nip19Bech32.decodePublicKey import com.vitorpamplona.quartz.utils.arrayOfNotNull import com.vitorpamplona.quartz.utils.bytesUsedInMemory @@ -62,13 +63,13 @@ data class ContactTag( @JvmStatic fun parse(tag: Array): ContactTag? { - if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME || tag[1].length != 64) 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 + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME || tag[1].length != 64) return null return try { ContactTag(decodePublicKey(tag[1]).toHexKey(), tag.getOrNull(2), tag.getOrNull(3)) } catch (e: Exception) { @@ -77,9 +78,15 @@ data class ContactTag( } } + @JvmStatic + fun parseKey(tag: Array): String? { + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME || tag[1].length != 64) return null + return tag[1] + } + @JvmStatic fun parseValidKey(tag: Array): String? { - if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME || tag[1].length != 64) return null return try { decodePublicKey(tag[1]).toHexKey() } catch (e: Exception) { @@ -88,6 +95,12 @@ data class ContactTag( } } + @JvmStatic + fun parseAsHint(tag: Array): PubKeyHint? { + if (tag.size < 3 || tag[0] != TAG_NAME || tag[1].length != 64 || tag[2].isEmpty()) return null + return PubKeyHint(tag[1], tag[2]) + } + @JvmStatic fun assemble( pubkey: HexKey, 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 f11057a247..bcd82b3fb2 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/OtsEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/OtsEvent.kt @@ -26,10 +26,9 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle +import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider 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.nip03Timestamp.tags.TargetEventTag import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.utils.TimeUtils import java.util.Base64 @@ -42,10 +41,13 @@ class OtsEvent( tags: Array>, content: String, sig: HexKey, -) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { +) : Event(id, pubKey, createdAt, KIND, tags, content, sig), + EventHintProvider { + override fun eventHints() = tags.mapNotNull(TargetEventTag::parseAsHint) + override fun isContentEncoded() = true - fun digestEventId() = tags.firstNotNullOfOrNull(ETag::parseId) + fun digestEventId() = tags.firstNotNullOfOrNull(TargetEventTag::parseId) fun otsByteArray(): ByteArray = decodeOtsState(content) @@ -82,7 +84,7 @@ class OtsEvent( initializer: TagArrayBuilder.() -> Unit = {}, ) = eventTemplate(KIND, encodeOtsState(otsState), createdAt) { alt(ALT) - eTag(ETag(eventId)) + targetEvent(TargetEventTag(eventId)) initializer() } @@ -95,8 +97,8 @@ class OtsEvent( ) = eventTemplate(KIND, encodeOtsState(otsState), createdAt) { alt(ALT) - eTag(event.toETag()) - kind(event.event.kind) + targetEvent(event.toETag()) + targetKind(event.event.kind) initializer() } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/TagArrayBuilderExt.kt new file mode 100644 index 0000000000..c42a2296a7 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/TagArrayBuilderExt.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.nip03Timestamp + +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip03Timestamp.tags.TargetEventKindTag +import com.vitorpamplona.quartz.nip03Timestamp.tags.TargetEventTag + +fun TagArrayBuilder.targetEvent(tag: TargetEventTag) = add(tag.toTagArray()) + +fun TagArrayBuilder.targetEvents(tag: List) = addAll(tag.map { it.toTagArray() }) + +fun TagArrayBuilder.targetKind(kind: Int) = add(TargetEventKindTag.assemble(kind)) + +fun TagArrayBuilder.targetKinds(kinds: List) = addAll(TargetEventKindTag.assemble(kinds)) + +fun TagArrayBuilder.targetKinds(kinds: Set) = addAll(TargetEventKindTag.assemble(kinds)) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/tags/TargetEventKindTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/tags/TargetEventKindTag.kt new file mode 100644 index 0000000000..6f2955a3d9 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/tags/TargetEventKindTag.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.nip03Timestamp.tags + +import com.vitorpamplona.quartz.nip01Core.tags.kinds.KindTag + +typealias TargetEventKindTag = KindTag diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/tags/TargetEventTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/tags/TargetEventTag.kt new file mode 100644 index 0000000000..10b3270b73 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/tags/TargetEventTag.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.nip03Timestamp.tags + +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag + +typealias TargetEventTag = ETag 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 1249a06e5b..7e79e267ad 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip09Deletions/DeletionEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip09Deletions/DeletionEvent.kt @@ -25,6 +25,8 @@ import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.addressables.aTag @@ -48,7 +50,13 @@ class DeletionEvent( tags: Array>, content: String, sig: HexKey, -) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { +) : Event(id, pubKey, createdAt, KIND, tags, content, sig), + EventHintProvider, + AddressHintProvider { + override fun eventHints() = tags.mapNotNull(ETag::parseAsHint) + + override fun addressHints() = tags.mapNotNull(ATag::parseAsHint) + fun deleteEvents() = taggedEvents() fun deleteEventIds() = taggedEventIds() diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/BaseThreadedEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/BaseThreadedEvent.kt index 5123f0c07d..72db040c05 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/BaseThreadedEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/BaseThreadedEvent.kt @@ -23,12 +23,18 @@ package com.vitorpamplona.quartz.nip10Notes import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider +import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedATags +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag 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.nip10Notes.content.findNostrUris import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag +import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress import com.vitorpamplona.quartz.nip19Bech32.entities.NEmbed import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent @@ -48,7 +54,16 @@ open class BaseThreadedEvent( tags: Array>, content: String, sig: HexKey, -) : Event(id, pubKey, createdAt, kind, tags, content, sig) { +) : Event(id, pubKey, createdAt, kind, tags, content, sig), + EventHintProvider, + AddressHintProvider, + PubKeyHintProvider { + override fun eventHints() = tags.mapNotNull(MarkedETag::parseAsHint) + tags.mapNotNull(QTag::parseEventAsHint) + + override fun addressHints() = tags.mapNotNull(ATag::parseAsHint) + tags.mapNotNull(QTag::parseAddressAsHint) + + override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) + fun mentions() = taggedUsers() fun markedRoot() = tags.firstNotNullOfOrNull(MarkedETag::parseRoot) 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 index 1c3ca60652..dc150479c3 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/tags/MarkedETag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/tags/MarkedETag.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.quartz.nip10Notes.tags import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint import com.vitorpamplona.quartz.nip01Core.tags.events.GenericETag import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent import com.vitorpamplona.quartz.utils.arrayOfNotNull @@ -180,6 +181,12 @@ data class MarkedETag( null } + @JvmStatic + fun parseAsHint(tag: Array): EventIdHint? { + if (tag.size < 3 || tag[0] != TAG_NAME || tag[1].length != 64 || tag[2].isEmpty()) return null + return EventIdHint(tag[1], tag[2]) + } + @JvmStatic fun parseRoot(tag: Array): MarkedETag? { if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null 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 index b12f689f35..975328dd79 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/base/BaseDMGroupEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/base/BaseDMGroupEvent.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.quartz.nip17Dm.base import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip59Giftwrap.WrappedEvent import kotlinx.collections.immutable.toImmutableSet @@ -37,7 +38,10 @@ open class BaseDMGroupEvent( sig: HexKey, ) : WrappedEvent(id, pubKey, createdAt, kind, tags, content, sig), ChatroomKeyable, - NIP17Group { + NIP17Group, + PubKeyHintProvider { + override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) + /** Recipients intended to receive this conversation */ fun recipients() = tags.mapNotNull(PTag::parse) 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 index 0d2e616232..13ebd81fef 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/files/ChatMessageEncryptedFileHeaderEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/files/ChatMessageEncryptedFileHeaderEvent.kt @@ -37,7 +37,7 @@ 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.HashSha256Tag import com.vitorpamplona.quartz.nip94FileMetadata.tags.OriginalHashTag import com.vitorpamplona.quartz.nip94FileMetadata.tags.SizeTag import com.vitorpamplona.quartz.utils.TimeUtils @@ -57,7 +57,7 @@ class ChatMessageEncryptedFileHeaderEvent( fun mimeType() = tags.firstNotNullOfOrNull(FileTypeTag::parse) - fun hash() = tags.firstNotNullOfOrNull(HashTag::parse) + fun hash() = tags.firstNotNullOfOrNull(HashSha256Tag::parse) fun size() = tags.firstNotNullOfOrNull(SizeTag::parse) 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 index 36d4001f0b..4ea99d0601 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/files/TagArrayBuilderExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/files/TagArrayBuilderExt.kt @@ -35,7 +35,7 @@ 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.HashSha256Tag import com.vitorpamplona.quartz.nip94FileMetadata.tags.ImageTag import com.vitorpamplona.quartz.nip94FileMetadata.tags.MagnetTag import com.vitorpamplona.quartz.nip94FileMetadata.tags.OriginalHashTag @@ -61,7 +61,7 @@ fun TagArrayBuilder.encryptionNonce(nonce: fun TagArrayBuilder.mimeType(mimeType: String) = add(FileTypeTag.assemble(mimeType)) -fun TagArrayBuilder.hash(hash: HexKey) = add(HashTag.assemble(hash)) +fun TagArrayBuilder.hash(hash: HexKey) = add(HashSha256Tag.assemble(hash)) fun TagArrayBuilder.fileSize(size: Int) = add(SizeTag.assemble(size)) 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 index 6abc59ce9c..b34eb627ef 100644 --- 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 @@ -38,6 +38,6 @@ class EncryptionNonce { } @JvmStatic - fun assemble(nonce: ByteArray) = arrayOf(EncryptionKey.TAG_NAME, nonce.toHexKey()) + fun assemble(nonce: ByteArray) = arrayOf(TAG_NAME, nonce.toHexKey()) } } 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 b392485b2c..581e66a5d8 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/GenericRepostEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/GenericRepostEvent.kt @@ -25,8 +25,12 @@ import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner 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.events.ETag import com.vitorpamplona.quartz.nip01Core.tags.events.eTag @@ -45,10 +49,29 @@ class GenericRepostEvent( tags: Array>, content: String, sig: HexKey, -) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { - fun boostedPost() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } +) : Event(id, pubKey, createdAt, KIND, tags, content, sig), + EventHintProvider, + PubKeyHintProvider, + AddressHintProvider { + override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) - fun originalAuthor() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } + override fun eventHints() = tags.mapNotNull(ETag::parseAsHint) + + override fun addressHints() = tags.mapNotNull(ATag::parseAsHint) + + fun boostedEvents() = tags.mapNotNull(ETag::parse) + + fun boostedATags() = tags.mapNotNull(ATag::parse) + + fun boostedAddresses() = tags.mapNotNull(ATag::parseAddress) + + fun originalAuthors() = tags.mapNotNull(PTag::parse) + + fun boostedEventIds() = tags.mapNotNull(ETag::parseId) + + fun boostedAddressIds() = tags.mapNotNull(ATag::parseAddressId) + + fun originalAuthorKeys() = tags.mapNotNull(PTag::parseKey) fun containedPost() = try { 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 97efab8013..691ec35c60 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/RepostEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/RepostEvent.kt @@ -25,15 +25,17 @@ import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider 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.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.alt import com.vitorpamplona.quartz.utils.TimeUtils @@ -45,10 +47,29 @@ class RepostEvent( tags: Array>, content: String, sig: HexKey, -) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { - fun boostedPost() = taggedEvents() +) : Event(id, pubKey, createdAt, KIND, tags, content, sig), + EventHintProvider, + PubKeyHintProvider, + AddressHintProvider { + override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) - fun originalAuthor() = taggedUsers() + override fun eventHints() = tags.mapNotNull(ETag::parseAsHint) + + override fun addressHints() = tags.mapNotNull(ATag::parseAsHint) + + fun boostedEvents() = tags.mapNotNull(ETag::parse) + + fun boostedATags() = tags.mapNotNull(ATag::parse) + + fun boostedAddresses() = tags.mapNotNull(ATag::parseAddress) + + fun originalAuthors() = tags.mapNotNull(PTag::parse) + + fun boostedEventIds() = tags.mapNotNull(ETag::parseId) + + fun boostedAddressIds() = tags.mapNotNull(ATag::parseAddressId) + + fun originalAuthorKeys() = tags.mapNotNull(PTag::parseKey) fun containedPost() = try { 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 index f600ecf2e1..52e6d14904 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/QTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/QTag.kt @@ -20,6 +20,9 @@ */ package com.vitorpamplona.quartz.nip18Reposts.quotes +import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint +import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint + interface QTag { fun toTagArray(): Array @@ -36,5 +39,17 @@ interface QTag { QAddressableTag.parse(tag) } } + + @JvmStatic + fun parseEventAsHint(tag: Array): EventIdHint? { + if (tag.size < 3 || tag[0] != TAG_NAME || tag[1].length != 64 || tag[2].isEmpty()) return null + return EventIdHint(tag[1], tag[2]) + } + + @JvmStatic + fun parseAddressAsHint(tag: Array): AddressHint? { + if (tag.size < 3 || tag[0] != TAG_NAME || tag[1].length == 64 || !tag[1].contains(':') || tag[2].isEmpty()) return null + return AddressHint(tag[1], tag[2]) + } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/bech32/Bech32Util.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/bech32/Bech32Util.kt index 77654be97a..093121ae5a 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/bech32/Bech32Util.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/bech32/Bech32Util.kt @@ -51,6 +51,12 @@ object Bech32 { const val ALPHABET: String = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" const val ALPHABET_UPPERCASE: String = "QPZRY9X8GF2TVDW0S3JN54KHCE6MUA7L" + private const val GEN0 = 0x3b6a57b2 + private const val GEN1 = 0x26508e6d + private const val GEN2 = 0x1ea119fa + private const val GEN3 = 0x3d4233dd + private const val GEN4 = 0x2a1462b3 + enum class Encoding( val constant: Int, ) { @@ -74,17 +80,17 @@ object Bech32 { fun expand(hrp: String): Array { val half = hrp.length + 1 val size = half + hrp.length + val firstPart = hrp.indices + val secondPart = half until size return Array(size) { when (it) { - in hrp.indices -> hrp[it].code.shr(5).toByte() - in half until size -> (hrp[it - half].code and 31).toByte() + in firstPart -> hrp[it].code.shr(5).toByte() + in secondPart -> (hrp[it - half].code and 31).toByte() else -> 0 } } } - private val GEN = arrayOf(0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3) - fun polymod( values: Array, values1: Array, @@ -93,16 +99,20 @@ object Bech32 { values.forEach { v -> val b = chk shr 25 chk = ((chk and 0x1ffffff) shl 5) xor v.toInt() - for (i in 0..4) { - if (((b shr i) and 1) != 0) chk = chk xor GEN[i] - } + if (((b shr 0) and 1) != 0) chk = chk xor GEN0 + if (((b shr 1) and 1) != 0) chk = chk xor GEN1 + if (((b shr 2) and 1) != 0) chk = chk xor GEN2 + if (((b shr 3) and 1) != 0) chk = chk xor GEN3 + if (((b shr 4) and 1) != 0) chk = chk xor GEN4 } values1.forEach { v -> val b = chk shr 25 chk = ((chk and 0x1ffffff) shl 5) xor v.toInt() - for (i in 0..4) { - if (((b shr i) and 1) != 0) chk = chk xor GEN[i] - } + if (((b shr 0) and 1) != 0) chk = chk xor GEN0 + if (((b shr 1) and 1) != 0) chk = chk xor GEN1 + if (((b shr 2) and 1) != 0) chk = chk xor GEN2 + if (((b shr 3) and 1) != 0) chk = chk xor GEN3 + if (((b shr 4) and 1) != 0) chk = chk xor GEN4 } return chk } @@ -128,8 +138,7 @@ object Bech32 { else -> addChecksum(hrp, int5s, encoding) } - val charArray = - CharArray(dataWithChecksum.size) { ALPHABET[dataWithChecksum[it].toInt()] }.concatToString() + val charArray = CharArray(dataWithChecksum.size) { ALPHABET[dataWithChecksum[it].toInt()] }.concatToString() return hrp + "1" + charArray } @@ -231,8 +240,8 @@ object Bech32 { @JvmStatic public fun eight2five(input: ByteArray): ArrayList { var buffer = 0L - val output = - ArrayList(input.size * 2) // larger array on purpose. Checksum is added later. + // larger array on purpose. Checksum is added later. + val output = ArrayList(input.size * 2) var count = 0 input.forEach { b -> buffer = (buffer shl 8) or (b.toLong() and 0xff) 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 c65fd1a34f..83e94ac49a 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/CommentEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/CommentEvent.kt @@ -25,7 +25,10 @@ import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle +import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent import com.vitorpamplona.quartz.nip21UriScheme.toNostrUri @@ -53,7 +56,16 @@ class CommentEvent( content: String, sig: HexKey, ) : BaseThreadedEvent(id, pubKey, createdAt, KIND, tags, content, sig), - RootScope { + RootScope, + EventHintProvider, + PubKeyHintProvider, + AddressHintProvider { + override fun pubKeyHints() = tags.mapNotNull(RootAuthorTag::parseAsHint) + tags.mapNotNull(ReplyAuthorTag::parseAsHint) + + override fun eventHints() = tags.mapNotNull(RootEventTag::parseAsHint) + tags.mapNotNull(ReplyEventTag::parseAsHint) + + override fun addressHints() = tags.mapNotNull(RootAddressTag::parseAsHint) + tags.mapNotNull(ReplyAddressTag::parseAsHint) + fun rootAuthor() = tags.firstNotNullOfOrNull(RootAuthorTag::parse) fun replyAuthor() = tags.firstNotNullOfOrNull(ReplyAuthorTag::parse) 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 index 7550a5e881..9e930bbf3a 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/ReplyAddressTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/ReplyAddressTag.kt @@ -25,6 +25,7 @@ import com.vitorpamplona.quartz.nip01Core.core.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.hints.types.AddressHint import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.utils.arrayOfNotNull @@ -51,6 +52,12 @@ class ReplyAddressTag( @JvmStatic fun parseAddress(tag: Array) = tag.valueIfMatches(TAG_NAME, TAG_SIZE) + @JvmStatic + fun parseAsHint(tag: Array): AddressHint? { + if (tag.size < 3 || tag[0] != TAG_NAME || tag[1].length == 64 || !tag[1].contains(':') || tag[2].isEmpty()) return null + return AddressHint(tag[1], tag[2]) + } + @JvmStatic fun assemble( addressId: HexKey, 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 index 48b6b1ade1..1f647c2d55 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/ReplyAuthorTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/ReplyAuthorTag.kt @@ -24,6 +24,7 @@ import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Tag import com.vitorpamplona.quartz.nip01Core.core.match +import com.vitorpamplona.quartz.nip01Core.hints.types.PubKeyHint import com.vitorpamplona.quartz.nip01Core.tags.people.PubKeyReferenceTag import com.vitorpamplona.quartz.utils.arrayOfNotNull @@ -55,6 +56,12 @@ data class ReplyAuthorTag( return tag[1] } + @JvmStatic + fun parseAsHint(tag: Array): PubKeyHint? { + if (tag.size < 3 || tag[0] != TAG_NAME || tag[1].length != 64 || tag[2].isEmpty()) return null + return PubKeyHint(tag[1], tag[2]) + } + @JvmStatic fun assemble( pubkey: HexKey, 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 index ac4c9da01f..77f68fbc42 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/ReplyEventTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/ReplyEventTag.kt @@ -25,6 +25,7 @@ import com.vitorpamplona.quartz.nip01Core.core.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.hints.types.EventIdHint import com.vitorpamplona.quartz.nip01Core.tags.events.EventReference import com.vitorpamplona.quartz.utils.arrayOfNotNull @@ -71,6 +72,12 @@ class ReplyEventTag( return tag[1] } + @JvmStatic + fun parseAsHint(tag: Array): EventIdHint? { + if (tag.size < 3 || tag[0] != TAG_NAME || tag[1].length != 64 || tag[2].isEmpty()) return null + return EventIdHint(tag[1], tag[2]) + } + @JvmStatic fun assemble( eventId: HexKey, 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 index c46dcf8651..32d6431512 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/RootAddressTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/RootAddressTag.kt @@ -25,6 +25,7 @@ import com.vitorpamplona.quartz.nip01Core.core.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.hints.types.AddressHint import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.utils.arrayOfNotNull @@ -69,6 +70,12 @@ class RootAddressTag( @JvmStatic fun parseAddress(tag: Array) = tag.valueIfMatches(TAG_NAME, TAG_SIZE) + @JvmStatic + fun parseAsHint(tag: Array): AddressHint? { + if (tag.size < 3 || tag[0] != TAG_NAME || tag[1].length == 64 || !tag[1].contains(':') || tag[2].isEmpty()) return null + return AddressHint(tag[1], tag[2]) + } + @JvmStatic fun assemble( addressId: HexKey, 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 index a24430433e..dcae0d9f93 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/RootAuthorTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/RootAuthorTag.kt @@ -22,10 +22,13 @@ package com.vitorpamplona.quartz.nip22Comments.tags import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.PUBKEY_LENGTH import com.vitorpamplona.quartz.nip01Core.core.Tag -import com.vitorpamplona.quartz.nip01Core.core.match +import com.vitorpamplona.quartz.nip01Core.hints.types.PubKeyHint import com.vitorpamplona.quartz.nip01Core.tags.people.PubKeyReferenceTag import com.vitorpamplona.quartz.utils.arrayOfNotNull +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.contract @Immutable data class RootAuthorTag( @@ -36,25 +39,45 @@ data class RootAuthorTag( companion object { const val TAG_NAME = "P" - const val TAG_SIZE = 2 @JvmStatic - fun match(tag: Tag) = tag.match(TAG_NAME, TAG_SIZE) + fun match(tag: Tag) = tag.size > 1 && tag[0] == TAG_NAME @JvmStatic fun parse(tag: Tag): ReplyAuthorTag? { - if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null - if (tag[1].length != 64) return null + if (tag.size < 2 || tag[0] != TAG_NAME || tag[1].length != PUBKEY_LENGTH) return null + return ReplyAuthorTag(tag[1], tag.getOrNull(2)) + } + + @JvmStatic + fun parse3(tag: Tag): ReplyAuthorTag? { + ensure(tag.size >= 2) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].length == PUBKEY_LENGTH) { 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 + if (tag.size < 2 || tag[0] != TAG_NAME || tag[1].length != PUBKEY_LENGTH) return null return tag[1] } + @JvmStatic + fun parseAsHint(tag: Array): PubKeyHint? { + if (tag.size < 3 || tag[0] != TAG_NAME || tag[1].length != PUBKEY_LENGTH || tag[2].isEmpty()) return null + return PubKeyHint(tag[1], tag[2]) + } + + @OptIn(ExperimentalContracts::class) + inline fun ensure( + condition: Boolean, + exit: () -> Nothing, + ) { + contract { returns() implies condition } + if (!condition) exit() + } + @JvmStatic fun assemble( pubkey: HexKey, 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 index b15c2401e4..c0d546f929 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/RootEventTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/RootEventTag.kt @@ -25,6 +25,7 @@ import com.vitorpamplona.quartz.nip01Core.core.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.hints.types.EventIdHint import com.vitorpamplona.quartz.nip01Core.tags.events.EventReference import com.vitorpamplona.quartz.utils.arrayOfNotNull @@ -71,6 +72,12 @@ class RootEventTag( return tag[1] } + @JvmStatic + fun parseAsHint(tag: Array): EventIdHint? { + if (tag.size < 3 || tag[0] != TAG_NAME || tag[1].length != 64 || tag[2].isEmpty()) return null + return EventIdHint(tag[1], tag[2]) + } + @JvmStatic fun assemble( eventId: HexKey, 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 f5195f4649..2c3d47f9ab 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip23LongContent/LongTextNoteEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip23LongContent/LongTextNoteEvent.kt @@ -24,12 +24,17 @@ import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider 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.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent +import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag import com.vitorpamplona.quartz.nip23LongContent.tags.ImageTag import com.vitorpamplona.quartz.nip23LongContent.tags.PublishedAtTag import com.vitorpamplona.quartz.nip23LongContent.tags.SummaryTag @@ -47,7 +52,16 @@ class LongTextNoteEvent( content: String, sig: HexKey, ) : BaseThreadedEvent(id, pubKey, createdAt, KIND, tags, content, sig), - AddressableEvent { + AddressableEvent, + EventHintProvider, + PubKeyHintProvider, + AddressHintProvider { + override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) + + override fun eventHints() = tags.mapNotNull(QTag::parseEventAsHint) + + override fun addressHints() = tags.mapNotNull(QTag::parseAddressAsHint) + override fun dTag() = tags.dTag() override fun aTag(relayHint: String?) = ATag(kind, pubKey, dTag(), relayHint) 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 9f59203549..6abac57172 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip25Reactions/ReactionEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip25Reactions/ReactionEvent.kt @@ -24,8 +24,12 @@ import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle +import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider 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.events.ETag import com.vitorpamplona.quartz.nip01Core.tags.events.eTag @@ -44,7 +48,16 @@ class ReactionEvent( tags: Array>, content: String, sig: HexKey, -) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { +) : Event(id, pubKey, createdAt, KIND, tags, content, sig), + EventHintProvider, + PubKeyHintProvider, + AddressHintProvider { + override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) + + override fun eventHints() = tags.mapNotNull(ETag::parseAsHint) + + override fun addressHints() = tags.mapNotNull(ATag::parseAsHint) + fun originalPost() = tags.mapNotNull(ETag::parseId) fun originalAuthor() = tags.mapNotNull(PTag::parseKey) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelCreateEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelCreateEvent.kt index 1b1e015e05..fc7f93afd6 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelCreateEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelCreateEvent.kt @@ -24,6 +24,8 @@ import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip28PublicChat.base.ChannelData import com.vitorpamplona.quartz.nip31Alts.alt @@ -37,7 +39,10 @@ class ChannelCreateEvent( tags: Array>, content: String, sig: HexKey, -) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { +) : Event(id, pubKey, createdAt, KIND, tags, content, sig), + EventHintProvider { + override fun eventHints() = channelInfo().relays?.map { EventIdHint(id, it) } ?: emptyList() + fun channelInfo() = ChannelData.parse(content) ?: ChannelData() companion object { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelHideMessageEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelHideMessageEvent.kt index 612c2625ad..1c4c3e0d8e 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelHideMessageEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelHideMessageEvent.kt @@ -24,6 +24,7 @@ import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle +import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.events.ETag import com.vitorpamplona.quartz.nip01Core.tags.events.eTags @@ -41,7 +42,10 @@ class ChannelHideMessageEvent( tags: Array>, content: String, sig: HexKey, -) : BasePublicChatEvent(id, pubKey, createdAt, KIND, tags, content, sig) { +) : BasePublicChatEvent(id, pubKey, createdAt, KIND, tags, content, sig), + EventHintProvider { + override fun eventHints() = tags.mapNotNull(ETag::parseAsHint) + fun eventsToHide() = tags.taggedEventIds() companion object { 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 index 2caac83ed9..bc98fc090a 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelMetadataEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelMetadataEvent.kt @@ -24,6 +24,8 @@ import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle +import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.events.ETag import com.vitorpamplona.quartz.nip28PublicChat.base.BasePublicChatEvent @@ -40,7 +42,10 @@ class ChannelMetadataEvent( tags: Array>, content: String, sig: HexKey, -) : BasePublicChatEvent(id, pubKey, createdAt, KIND, tags, content, sig) { +) : BasePublicChatEvent(id, pubKey, createdAt, KIND, tags, content, sig), + EventHintProvider { + override fun eventHints() = channelInfo().relays?.map { EventIdHint(id, it) } ?: emptyList() + fun channelInfo() = ChannelData.parse(content) ?: ChannelData() companion object { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelMuteUserEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelMuteUserEvent.kt index 101c848adc..2211dbd176 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelMuteUserEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelMuteUserEvent.kt @@ -24,6 +24,7 @@ import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle +import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip01Core.tags.people.pTags @@ -41,7 +42,10 @@ class ChannelMuteUserEvent( tags: Array>, content: String, sig: HexKey, -) : BasePublicChatEvent(id, pubKey, createdAt, KIND, tags, content, sig) { +) : BasePublicChatEvent(id, pubKey, createdAt, KIND, tags, content, sig), + PubKeyHintProvider { + override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) + fun usersToMute() = tags.taggedUserIds() companion object { 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 index 80cbb9139d..10c565b704 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip30CustomEmoji/selection/EmojiPackSelectionEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip30CustomEmoji/selection/EmojiPackSelectionEvent.kt @@ -24,9 +24,11 @@ import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.BaseReplaceableEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle 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.Address import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedATags import com.vitorpamplona.quartz.nip30CustomEmoji.pack.EmojiPackEvent @@ -41,7 +43,10 @@ class EmojiPackSelectionEvent( tags: Array>, content: String, sig: HexKey, -) : BaseReplaceableEvent(id, pubKey, createdAt, KIND, tags, content, sig) { +) : BaseReplaceableEvent(id, pubKey, createdAt, KIND, tags, content, sig), + AddressHintProvider { + override fun addressHints() = tags.mapNotNull(ATag::parseAsHint) + fun emojiPacks() = taggedATags() companion object { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/issue/GitIssueEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/issue/GitIssueEvent.kt index bf59e87107..063aafcf25 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/issue/GitIssueEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/issue/GitIssueEvent.kt @@ -23,13 +23,17 @@ package com.vitorpamplona.quartz.nip34Git.issue import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle +import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag 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.nip18Reposts.quotes.QTag import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent import com.vitorpamplona.quartz.utils.TimeUtils @@ -42,7 +46,16 @@ class GitIssueEvent( tags: Array>, content: String, sig: HexKey, -) : BaseThreadedEvent(id, pubKey, createdAt, KIND, tags, content, sig) { +) : BaseThreadedEvent(id, pubKey, createdAt, KIND, tags, content, sig), + PubKeyHintProvider, + EventHintProvider, + AddressHintProvider { + override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) + + override fun eventHints() = tags.mapNotNull(QTag::parseEventAsHint) + + override fun addressHints() = tags.mapNotNull(ATag::parseAsHint) + tags.mapNotNull(QTag::parseAddressAsHint) + fun repositoryHex() = tags.firstNotNullOfOrNull(ATag::parseAddressId) fun repositoryAddress() = tags.firstNotNullOfOrNull(ATag::parseAddress) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/patch/GitPatchEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/patch/GitPatchEvent.kt index d0a91b5b20..b9366cbd51 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/patch/GitPatchEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/patch/GitPatchEvent.kt @@ -23,9 +23,14 @@ package com.vitorpamplona.quartz.nip34Git.patch import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider 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.people.PTag +import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.utils.TimeUtils @@ -37,7 +42,16 @@ class GitPatchEvent( tags: Array>, content: String, sig: HexKey, -) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { +) : Event(id, pubKey, createdAt, KIND, tags, content, sig), + PubKeyHintProvider, + EventHintProvider, + AddressHintProvider { + override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) + + override fun eventHints() = tags.mapNotNull(MarkedETag::parseAsHint) + + override fun addressHints() = tags.mapNotNull(ATag::parseAsHint) + private fun innerRepository() = tags.firstOrNull { it.size > 3 && it[0] == "a" && it[3] == "root" } ?: tags.firstOrNull { it.size > 1 && it[0] == "a" } 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 index f7dd714120..430b4a597a 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/reply/GitReplyEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/reply/GitReplyEvent.kt @@ -23,13 +23,18 @@ package com.vitorpamplona.quartz.nip34Git.reply import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle +import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag 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.nip18Reposts.quotes.QTag import com.vitorpamplona.quartz.nip34Git.issue.GitIssueEvent import com.vitorpamplona.quartz.nip34Git.patch.GitPatchEvent import com.vitorpamplona.quartz.utils.TimeUtils @@ -43,7 +48,16 @@ class GitReplyEvent( tags: Array>, content: String, sig: HexKey, -) : BaseThreadedEvent(id, pubKey, createdAt, KIND, tags, content, sig) { +) : BaseThreadedEvent(id, pubKey, createdAt, KIND, tags, content, sig), + PubKeyHintProvider, + EventHintProvider, + AddressHintProvider { + override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) + + override fun eventHints() = tags.mapNotNull(MarkedETag::parseAsHint) + tags.mapNotNull(QTag::parseEventAsHint) + + override fun addressHints() = tags.mapNotNull(ATag::parseAsHint) + tags.mapNotNull(QTag::parseAddressAsHint) + fun repositoryHex() = tags.firstNotNullOfOrNull(ATag::parseAddressId) fun repository() = tags.firstNotNullOfOrNull(ATag::parse) 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 9389eab686..a5cabb1342 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip35Torrents/TorrentCommentEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip35Torrents/TorrentCommentEvent.kt @@ -23,15 +23,20 @@ package com.vitorpamplona.quartz.nip35Torrents import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle +import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider 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.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag import com.vitorpamplona.quartz.nip10Notes.tags.positionalMarkedTags +import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.utils.TimeUtils @@ -43,7 +48,16 @@ class TorrentCommentEvent( tags: Array>, content: String, sig: HexKey, -) : BaseThreadedEvent(id, pubKey, createdAt, KIND, tags, content, sig) { +) : BaseThreadedEvent(id, pubKey, createdAt, KIND, tags, content, sig), + EventHintProvider, + PubKeyHintProvider, + AddressHintProvider { + override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) + + override fun eventHints() = tags.mapNotNull(ETag::parseAsHint) + tags.mapNotNull(QTag::parseEventAsHint) + + override fun addressHints() = tags.mapNotNull(QTag::parseAddressAsHint) + fun torrent() = tags.firstNotNullOfOrNull(MarkedETag::parseRoot) ?: tags.firstNotNullOfOrNull(ETag::parse) fun torrentIds() = tags.firstNotNullOfOrNull(MarkedETag::parseRootId) ?: tags.firstNotNullOfOrNull(ETag::parseId) 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 1f1f16f51b..aa3827a90d 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/GeneralListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/GeneralListEvent.kt @@ -23,14 +23,19 @@ package com.vitorpamplona.quartz.nip51Lists import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.isTagged +import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider 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.nip01Core.tags.addressables.Address -import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEvents +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag 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 com.vitorpamplona.quartz.nip01Core.tags.hashtags.HashtagTag +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.nip51Lists.tags.NameTag +import com.vitorpamplona.quartz.nip51Lists.tags.TitleTag import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.toImmutableSet @@ -43,20 +48,22 @@ abstract class GeneralListEvent( tags: Array>, content: String, sig: HexKey, -) : PrivateTagArrayEvent(id, pubKey, createdAt, kind, tags, content, sig) { - fun category() = dTag() +) : PrivateTagArrayEvent(id, pubKey, createdAt, kind, tags, content, sig), + EventHintProvider, + AddressHintProvider, + PubKeyHintProvider { + override fun eventHints() = tags.mapNotNull(ETag::parseAsHint) + (cachedPrivateTags()?.mapNotNull(ETag::parseAsHint) ?: emptyList()) - fun bookmarkedPosts() = taggedEvents() + override fun addressHints() = tags.mapNotNull(ATag::parseAsHint) + (cachedPrivateTags()?.mapNotNull(ATag::parseAsHint) ?: emptyList()) - fun bookmarkedPeople() = taggedUsers() + override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) + (cachedPrivateTags()?.mapNotNull(PTag::parseAsHint) ?: emptyList()) - fun bookmarkedPeopleIds() = taggedUserIds() + fun name() = tags.firstNotNullOfOrNull(NameTag::parse) - fun name() = tags.firstOrNull { it.size > 1 && it[0] == "name" }?.get(1) + @Deprecated("NIP-51 has deprecated Title. Use name instead", ReplaceWith("name()")) + fun title() = tags.firstNotNullOfOrNull(TitleTag::parse) - fun title() = tags.firstOrNull { it.size > 1 && it[0] == "title" }?.get(1) - - fun nameOrTitle() = name()?.ifBlank { null } ?: title()?.ifBlank { null } + fun nameOrTitle() = name() ?: title() fun filterTagList( key: String, @@ -124,13 +131,13 @@ abstract class GeneralListEvent( onReady: (List
) -> Unit, ) = privateTags(signer) { onReady(filterAddresses(it)) } - fun filterUsers(tags: Array>): List = tags.filter { it.size > 1 && it[0] == "p" }.map { it[1] } + fun filterUsers(tags: Array>): List = tags.mapNotNull(PTag::parseKey) - fun filterHashtags(tags: Array>): List = tags.filter { it.size > 1 && it[0] == "t" }.map { it[1] } + fun filterHashtags(tags: Array>): List = tags.mapNotNull(HashtagTag::parse) fun filterGeohashes(tags: Array>): List = tags.geohashes() - fun filterEvents(tags: Array>): List = tags.filter { it.size > 1 && it[0] == "e" }.map { it[1] } + fun filterEvents(tags: Array>): List = tags.mapNotNull(ETag::parseId) fun filterATags(tags: Array>): List = tags.mapNotNull(ATag::parse) 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 1148fb1b8f..844026e78e 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/MuteListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/MuteListEvent.kt @@ -26,9 +26,6 @@ import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.utils.TimeUtils -import com.vitorpamplona.quartz.utils.bytesUsedInMemory -import com.vitorpamplona.quartz.utils.pointerSizeInBytes -import kotlinx.collections.immutable.ImmutableSet @Immutable class MuteListEvent( @@ -39,39 +36,16 @@ class MuteListEvent( content: String, sig: HexKey, ) : GeneralListEvent(id, pubKey, createdAt, KIND, tags, content, sig) { - @Transient var publicAndPrivateUserCache: ImmutableSet? = null - - @Transient var publicAndPrivateWordCache: ImmutableSet? = null - - override fun countMemory(): Long = - super.countMemory() + - pointerSizeInBytes + (publicAndPrivateUserCache?.sumOf { pointerSizeInBytes + it.bytesUsedInMemory() } ?: 0) + - pointerSizeInBytes + (publicAndPrivateWordCache?.sumOf { pointerSizeInBytes + it.bytesUsedInMemory() } ?: 0) - override fun dTag() = FIXED_D_TAG fun publicAndPrivateUsersAndWords( signer: NostrSigner, onReady: (PeopleListEvent.UsersAndWords) -> Unit, ) { - publicAndPrivateUserCache?.let { userList -> - publicAndPrivateWordCache?.let { wordList -> - onReady(PeopleListEvent.UsersAndWords(userList, wordList)) - return - } - } - privateTagsOrEmpty(signer) { - publicAndPrivateUserCache = filterTagList("p", it) - publicAndPrivateWordCache = filterTagList("word", it) - - publicAndPrivateUserCache?.let { userList -> - publicAndPrivateWordCache?.let { wordList -> - onReady( - PeopleListEvent.UsersAndWords(userList, wordList), - ) - } - } + onReady( + PeopleListEvent.UsersAndWords(filterTagList("p", it), filterTagList("word", it)), + ) } } 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 1fa5058e5a..6cd6ad7e96 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/PeopleListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/PeopleListEvent.kt @@ -26,9 +26,6 @@ import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.utils.TimeUtils -import com.vitorpamplona.quartz.utils.bytesUsedInMemory -import com.vitorpamplona.quartz.utils.pointerSizeInBytes -import kotlinx.collections.immutable.ImmutableSet @Immutable class PeopleListEvent( @@ -39,45 +36,6 @@ class PeopleListEvent( content: String, sig: HexKey, ) : GeneralListEvent(id, pubKey, createdAt, KIND, tags, content, sig) { - @Transient var publicAndPrivateUserCache: ImmutableSet? = null - - @Transient var publicAndPrivateWordCache: ImmutableSet? = null - - override fun countMemory(): Long = - super.countMemory() + - pointerSizeInBytes + (publicAndPrivateUserCache?.sumOf { pointerSizeInBytes + it.bytesUsedInMemory() } ?: 0) + - pointerSizeInBytes + (publicAndPrivateWordCache?.sumOf { pointerSizeInBytes + it.bytesUsedInMemory() } ?: 0) - - fun publicAndPrivateWords( - signer: NostrSigner, - onReady: (ImmutableSet) -> Unit, - ) { - publicAndPrivateWordCache?.let { - onReady(it) - return - } - - privateTagsOrEmpty(signer) { - publicAndPrivateWordCache = filterTagList("word", it) - publicAndPrivateWordCache?.let { onReady(it) } - } - } - - fun publicAndPrivateUsers( - signer: NostrSigner, - onReady: (ImmutableSet) -> Unit, - ) { - publicAndPrivateUserCache?.let { - onReady(it) - return - } - - privateTagsOrEmpty(signer) { - publicAndPrivateUserCache = filterTagList("p", it) - publicAndPrivateUserCache?.let { onReady(it) } - } - } - @Immutable class UsersAndWords( val users: Set = setOf(), @@ -88,24 +46,13 @@ class PeopleListEvent( signer: NostrSigner, onReady: (UsersAndWords) -> Unit, ) { - publicAndPrivateUserCache?.let { userList -> - publicAndPrivateWordCache?.let { wordList -> - onReady(UsersAndWords(userList, wordList)) - return - } - } - privateTagsOrEmpty(signer) { - publicAndPrivateUserCache = filterTagList("p", it) - publicAndPrivateWordCache = filterTagList("word", it) - - publicAndPrivateUserCache?.let { userList -> - publicAndPrivateWordCache?.let { wordList -> - onReady( - UsersAndWords(userList, wordList), - ) - } - } + onReady( + UsersAndWords( + filterTagList("p", it), + filterTagList("word", it), + ), + ) } } 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 cf8ea9147d..47ce039faa 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/PrivateTagArrayEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/PrivateTagArrayEvent.kt @@ -25,6 +25,7 @@ import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.pointerSizeInBytes @@ -63,7 +64,7 @@ abstract class PrivateTagArrayEvent( } try { - Nip51PrivateTags.decrypt(content, signer) { + PrivateTagsInContent.decrypt(content, signer) { privateTagsCache = it privateTagsCache?.let { onReady(it) } } @@ -78,7 +79,7 @@ abstract class PrivateTagArrayEvent( onReady: (content: String) -> Unit, ) { privateTags(signer) { privateTags -> - Nip51PrivateTags.encryptNip04( + PrivateTagsInContent.encryptNip04( privateTags = change(privateTags), signer = signer, ) { encryptedTags -> @@ -97,7 +98,7 @@ abstract class PrivateTagArrayEvent( ) { if (toPrivate) { current.privateTags(signer) { privateTags -> - Nip51PrivateTags.encryptNip04( + PrivateTagsInContent.encryptNip04( privateTags = privateTags.plus(newTag), signer = signer, ) { encryptedTags -> @@ -118,7 +119,7 @@ abstract class PrivateTagArrayEvent( ) { if (toPrivate) { current.privateTags(signer) { privateTags -> - Nip51PrivateTags.encryptNip04( + PrivateTagsInContent.encryptNip04( privateTags = privateTags.plus(newTag), signer = signer, ) { encryptedTags -> @@ -168,7 +169,7 @@ abstract class PrivateTagArrayEvent( onReady: (content: String, tags: Array>) -> Unit, ) { current.privateTags(signer) { privateTags -> - Nip51PrivateTags.encryptNip04( + PrivateTagsInContent.encryptNip04( privateTags = privateTags.replaceAll(oldTagStartsWith, newTag), signer = signer, ) { encryptedTags -> @@ -185,7 +186,7 @@ abstract class PrivateTagArrayEvent( onReady: (content: String, tags: Array>) -> Unit, ) { current.privateTags(signer) { privateTags -> - Nip51PrivateTags.encryptNip04( + PrivateTagsInContent.encryptNip04( privateTags = privateTags.remove(oldTagStartsWith), signer = signer, ) { encryptedTags -> @@ -201,7 +202,7 @@ abstract class PrivateTagArrayEvent( onReady: (content: String, tags: Array>) -> Unit, ) { current.privateTags(signer) { privateTags -> - Nip51PrivateTags.encryptNip04( + PrivateTagsInContent.encryptNip04( privateTags = privateTags.remove(oldTagStartsWith), signer = signer, ) { encryptedTags -> @@ -224,7 +225,7 @@ abstract class PrivateTagArrayEvent( onReady: (content: String, tags: Array>) -> Unit, ) { current.privateTags(signer) { privateTags -> - Nip51PrivateTags.encryptNip04( + PrivateTagsInContent.encryptNip04( privateTags = privateTags.remove(oldTagStartsWith), signer = signer, ) { encryptedTags -> @@ -239,7 +240,7 @@ abstract class PrivateTagArrayEvent( signer: NostrSigner, onReady: (content: String, tags: Array>) -> Unit, ) { - Nip51PrivateTags.encryptNip04( + PrivateTagsInContent.encryptNip04( privateTags = arrayOf(newTag), signer = signer, ) { encryptedTags -> diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/Nip51PrivateTags.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/encryption/PrivateTagsInContent.kt similarity index 96% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/Nip51PrivateTags.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/encryption/PrivateTagsInContent.kt index 78a72d1f8f..4e91e5e7a0 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/Nip51PrivateTags.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/encryption/PrivateTagsInContent.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.nip51Lists +package com.vitorpamplona.quartz.nip51Lists.encryption import com.fasterxml.jackson.module.kotlin.readValue import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -class Nip51PrivateTags { +class PrivateTagsInContent { companion object { fun decode(content: String) = EventMapper.mapper.readValue>>(content) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/tags/NameTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/tags/NameTag.kt new file mode 100644 index 0000000000..6935c0782a --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/tags/NameTag.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.nip51Lists.tags + +class NameTag { + companion object { + const val TAG_NAME = "name" + + @JvmStatic + fun parse(tag: Array): String? { + if (tag.size < 2 || tag[0] != TAG_NAME || tag[1].isEmpty()) return null + return tag[1] + } + + @JvmStatic + fun assemble(name: String) = arrayOf(TAG_NAME, name) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/tags/TitleTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/tags/TitleTag.kt new file mode 100644 index 0000000000..9b46e5ef55 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/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.nip51Lists.tags + +@Deprecated("Use NameTag Instead") +class TitleTag { + companion object { + const val TAG_NAME = "title" + + @JvmStatic + fun parse(tag: Array): String? { + if (tag.size < 2 || tag[0] != TAG_NAME || tag[1].isEmpty()) return null + return tag[1] + } + + @JvmStatic + fun assemble(name: String) = arrayOf(TAG_NAME, name) + } +} 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 index 8eba6cb104..65f82c8c30 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/chat/LiveActivitiesChatMessageEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/chat/LiveActivitiesChatMessageEvent.kt @@ -23,10 +23,16 @@ package com.vitorpamplona.quartz.nip53LiveActivities.chat import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle +import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider 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.people.PTag import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent +import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.utils.TimeUtils @@ -38,7 +44,16 @@ class LiveActivitiesChatMessageEvent( tags: Array>, content: String, sig: HexKey, -) : BaseThreadedEvent(id, pubKey, createdAt, KIND, tags, content, sig) { +) : BaseThreadedEvent(id, pubKey, createdAt, KIND, tags, content, sig), + EventHintProvider, + PubKeyHintProvider, + AddressHintProvider { + override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) + + override fun eventHints() = tags.mapNotNull(ETag::parseAsHint) + tags.mapNotNull(QTag::parseEventAsHint) + + override fun addressHints() = tags.mapNotNull(ATag::parseAsHint) + tags.mapNotNull(QTag::parseAddressAsHint) + private fun activityHex() = tags.firstNotNullOfOrNull(ATag::parseAddressId) fun activity() = tags.firstNotNullOfOrNull(ATag::parse) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip68Picture/IMetaTagBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip68Picture/IMetaTagBuilderExt.kt index 1ab4d044ca..620754dd7b 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip68Picture/IMetaTagBuilderExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip68Picture/IMetaTagBuilderExt.kt @@ -28,7 +28,7 @@ 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.HashSha256Tag import com.vitorpamplona.quartz.nip94FileMetadata.tags.ImageTag import com.vitorpamplona.quartz.nip94FileMetadata.tags.MagnetTag import com.vitorpamplona.quartz.nip94FileMetadata.tags.MimeTypeTag @@ -48,7 +48,7 @@ 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.hash(hash: HexKey) = add(HashSha256Tag.TAG_NAME, hash) fun IMetaTagBuilder.size(size: Int) = add(SizeTag.TAG_NAME, size.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 index a4a48131d4..30fe0ee2da 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip68Picture/IMetaTagExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip68Picture/IMetaTagExt.kt @@ -27,7 +27,7 @@ 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.HashSha256Tag import com.vitorpamplona.quartz.nip94FileMetadata.tags.ImageTag import com.vitorpamplona.quartz.nip94FileMetadata.tags.MagnetTag import com.vitorpamplona.quartz.nip94FileMetadata.tags.MimeTypeTag @@ -47,7 +47,7 @@ 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.hash() = properties.get(HashSha256Tag.TAG_NAME) fun IMetaTag.size() = properties.get(SizeTag.TAG_NAME) 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 bfa141b93e..d14368859e 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip68Picture/PictureEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip68Picture/PictureEvent.kt @@ -33,7 +33,7 @@ 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.HashSha256Tag import com.vitorpamplona.quartz.nip94FileMetadata.tags.ImageTag import com.vitorpamplona.quartz.nip94FileMetadata.tags.MagnetTag import com.vitorpamplona.quartz.nip94FileMetadata.tags.MimeTypeTag @@ -68,7 +68,7 @@ class PictureEvent( private fun mimeType() = tags.firstNotNullOfOrNull(MimeTypeTag::parse) - private fun hash() = tags.firstNotNullOfOrNull(HashTag::parse) + private fun hash() = tags.firstNotNullOfOrNull(HashSha256Tag::parse) private fun size() = tags.firstNotNullOfOrNull(SizeTag::parse) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip68Picture/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip68Picture/TagArrayBuilderExt.kt index 4458d16496..4c7c8cb5e4 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip68Picture/TagArrayBuilderExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip68Picture/TagArrayBuilderExt.kt @@ -24,7 +24,7 @@ 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.HashSha256Tag import com.vitorpamplona.quartz.nip94FileMetadata.tags.MimeTypeTag fun TagArrayBuilder.title(title: String) = addUnique(TitleTag.assemble(title)) @@ -43,7 +43,7 @@ fun TagArrayBuilder.pictureIMeta( fun TagArrayBuilder.pictureIMeta(imeta: PictureMeta): TagArrayBuilder { add(imeta.toIMetaArray()) - imeta.hash?.let { add(HashTag.assemble(it)) } + imeta.hash?.let { add(HashSha256Tag.assemble(it)) } imeta.mimeType?.let { add(MimeTypeTag.assemble(it)) } return this } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/IMetaTagBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/IMetaTagBuilderExt.kt index f8bbe904b0..c902f1f586 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/IMetaTagBuilderExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/IMetaTagBuilderExt.kt @@ -27,7 +27,7 @@ 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.HashSha256Tag import com.vitorpamplona.quartz.nip94FileMetadata.tags.ImageTag import com.vitorpamplona.quartz.nip94FileMetadata.tags.MagnetTag import com.vitorpamplona.quartz.nip94FileMetadata.tags.MimeTypeTag @@ -47,7 +47,7 @@ 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.hash(hash: HexKey) = add(HashSha256Tag.TAG_NAME, hash) fun IMetaTagBuilder.size(size: Int) = add(SizeTag.TAG_NAME, size.toString()) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/IMetaTagExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/IMetaTagExt.kt index 923d79029c..aa17ec54ac 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/IMetaTagExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/IMetaTagExt.kt @@ -26,7 +26,7 @@ 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.HashSha256Tag import com.vitorpamplona.quartz.nip94FileMetadata.tags.ImageTag import com.vitorpamplona.quartz.nip94FileMetadata.tags.MagnetTag import com.vitorpamplona.quartz.nip94FileMetadata.tags.MimeTypeTag @@ -46,7 +46,7 @@ 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.hash() = properties.get(HashSha256Tag.TAG_NAME) fun IMetaTag.size() = properties.get(SizeTag.TAG_NAME) 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 b2790e9b92..6b7588e5d1 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/VideoEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/VideoEvent.kt @@ -37,7 +37,7 @@ 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.HashSha256Tag import com.vitorpamplona.quartz.nip94FileMetadata.tags.ImageTag import com.vitorpamplona.quartz.nip94FileMetadata.tags.MagnetTag import com.vitorpamplona.quartz.nip94FileMetadata.tags.MimeTypeTag @@ -65,7 +65,7 @@ abstract class VideoEvent( private fun mimeType() = tags.firstNotNullOfOrNull(MimeTypeTag::parse) - private fun hash() = tags.firstNotNullOfOrNull(HashTag::parse) + private fun hash() = tags.firstNotNullOfOrNull(HashSha256Tag::parse) private fun size() = tags.firstNotNullOfOrNull(SizeTag::parse) 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 32f1b2cad9..d7abfef1a6 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/FileHeaderEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/FileHeaderEvent.kt @@ -30,7 +30,7 @@ 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.HashSha256Tag import com.vitorpamplona.quartz.nip94FileMetadata.tags.ImageTag import com.vitorpamplona.quartz.nip94FileMetadata.tags.MagnetTag import com.vitorpamplona.quartz.nip94FileMetadata.tags.MimeTypeTag @@ -57,7 +57,7 @@ class FileHeaderEvent( fun mimeType() = tags.firstNotNullOfOrNull(MimeTypeTag::parse) - fun hash() = tags.firstNotNullOfOrNull(HashTag::parse) + fun hash() = tags.firstNotNullOfOrNull(HashSha256Tag::parse) fun size() = tags.firstNotNullOfOrNull(SizeTag::parse) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/IMetaTagBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/IMetaTagBuilderExt.kt index 6cf3ca491a..6e9d4d3ab3 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/IMetaTagBuilderExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/IMetaTagBuilderExt.kt @@ -27,7 +27,7 @@ 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.HashSha256Tag import com.vitorpamplona.quartz.nip94FileMetadata.tags.ImageTag import com.vitorpamplona.quartz.nip94FileMetadata.tags.MagnetTag import com.vitorpamplona.quartz.nip94FileMetadata.tags.MimeTypeTag @@ -47,7 +47,7 @@ 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.hash(hash: HexKey) = add(HashSha256Tag.TAG_NAME, hash) fun IMetaTagBuilder.size(size: Int) = add(SizeTag.TAG_NAME, size.toString()) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/TagArrayBuilderExt.kt index 3a5c207fd2..98f29ae70f 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/TagArrayBuilderExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/TagArrayBuilderExt.kt @@ -25,7 +25,7 @@ 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.HashSha256Tag import com.vitorpamplona.quartz.nip94FileMetadata.tags.ImageTag import com.vitorpamplona.quartz.nip94FileMetadata.tags.MagnetTag import com.vitorpamplona.quartz.nip94FileMetadata.tags.MimeTypeTag @@ -41,7 +41,7 @@ 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.hash(hash: HexKey) = add(HashSha256Tag.assemble(hash)) fun TagArrayBuilder.fileSize(size: Int) = add(SizeTag.assemble(size)) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/HashTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/HashSha256Tag.kt similarity index 92% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/HashTag.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/HashSha256Tag.kt index 04f7d4a9ab..9880e9162a 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/HashTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip94FileMetadata/tags/HashSha256Tag.kt @@ -20,14 +20,14 @@ */ package com.vitorpamplona.quartz.nip94FileMetadata.tags -class HashTag { +class HashSha256Tag { 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 + if (tag.size < TAG_SIZE || tag[0] != TAG_NAME || tag[1].isEmpty()) return null return tag[1] } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/utils/Ensure.kt b/quartz/src/main/java/com/vitorpamplona/quartz/utils/Ensure.kt new file mode 100644 index 0000000000..c1ec0bce2e --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/utils/Ensure.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.utils + +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.contract + +@OptIn(ExperimentalContracts::class) +inline fun ensure( + condition: Boolean, + exit: () -> Nothing, +) { + contract { + returns() implies condition + } + if (!condition) exit() +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/utils/Hex.kt b/quartz/src/main/java/com/vitorpamplona/quartz/utils/Hex.kt index 9aeb6aa410..e1ae0e5002 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/utils/Hex.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/utils/Hex.kt @@ -21,24 +21,25 @@ package com.vitorpamplona.quartz.utils object Hex { - private val lowerCaseHex = arrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f') - private val upperCaseHex = arrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F') + private const val LOWER_CASE_HEX = "0123456789abcdef" + private const val UPPER_CASE_HEX = "0123456789ABCDEF" private val hexToByte: IntArray = IntArray(256) { -1 }.apply { - lowerCaseHex.forEachIndexed { index, char -> this[char.code] = index } - upperCaseHex.forEachIndexed { index, char -> this[char.code] = index } + LOWER_CASE_HEX.forEachIndexed { index, char -> this[char.code] = index } + UPPER_CASE_HEX.forEachIndexed { index, char -> this[char.code] = index } } // Encodes both chars in a single Int variable private val byteToHex = IntArray(256) { - (lowerCaseHex[(it shr 4)].code shl 8) or lowerCaseHex[(it and 0xF)].code + (LOWER_CASE_HEX[(it shr 4)].code shl 8) or LOWER_CASE_HEX[(it and 0xF)].code } @JvmStatic fun isHex(hex: String?): Boolean { - if (hex.isNullOrEmpty() || hex.length and 1 != 0) return false + if (hex.isNullOrEmpty()) return false + if (hex.length and 1 != 0) return false try { for (c in hex.indices) { From d5d289a8344cb00ae9d213149d58b1ab48f8b071 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 24 Feb 2025 20:46:33 -0500 Subject: [PATCH 45/76] Uses note.toEventUri to make sure the relay hint is correct Avoids crash when adding an event to gallery without a relay hint. --- .../amethyst/ui/components/ZoomableContentView.kt | 2 +- .../ui/note/types/ChatMessageEncryptedFile.kt | 2 +- .../amethyst/ui/note/types/Highlight.kt | 3 +-- .../ui/screen/loggedIn/chatrooms/ChannelScreen.kt | 3 +-- .../ui/screen/loggedIn/threadview/ThreadFeedView.kt | 13 +++++++++++++ .../vitorpamplona/quartz/nip19Bech32/EventExt.kt | 6 +++--- .../vitorpamplona/quartz/nip21UriScheme/EventExt.kt | 2 +- 7 files changed, 21 insertions(+), 10 deletions(-) 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 a7cf24bac6..2e991ee7ad 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 @@ -679,7 +679,7 @@ fun ShareImageAction( if (videoUri != null) { val n19 = Nip19Parser.uriToRoute(postNostrUri)?.entity as? NEvent if (n19 != null) { - accountViewModel.addMediaToGallery(n19.hex, videoUri, n19.relay[0], blurhash, dim, hash, mimeType) // TODO Whole list or first? + accountViewModel.addMediaToGallery(n19.hex, videoUri, n19.relay.getOrNull(0), blurhash, dim, hash, mimeType) // TODO Whole list or first? accountViewModel.toast(R.string.media_added, R.string.media_added_to_profile_gallery) } } 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 b621366692..f3413aff79 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 @@ -129,7 +129,7 @@ fun RenderEncryptedFile( hash = noteEvent.originalHash(), blurhash = noteEvent.blurhash(), dim = noteEvent.dimensions(), - uri = noteEvent.toNostrUri(), + uri = note.toNostrUri(), mimeType = mimeType, encryptionAlgo = algo, encryptionKey = key, 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 bfbe434940..06e0dbcdac 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 @@ -58,7 +58,6 @@ import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip01Core.tags.events.ETag import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent -import com.vitorpamplona.quartz.nip19Bech32.toNIP19 import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -260,7 +259,7 @@ fun DisplayEntryForNote( style = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.primary), ) } else { - DisplayEvent(noteEvent.id, noteEvent.kind, noteEvent.toNIP19(), null, accountViewModel, nav) + DisplayEvent(noteEvent.id, noteEvent.kind, note.toNostrUri(), null, accountViewModel, nav) } } 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 fa00e70a29..d58262f925 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 @@ -177,7 +177,6 @@ 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.nip28PublicChat.base.notify import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent import com.vitorpamplona.quartz.nip30CustomEmoji.emojis @@ -878,7 +877,7 @@ fun ShowVideoStreaming( description = baseChannel.toBestDisplayName(), artworkUri = event.image(), authorName = baseChannel.creatorName(), - uri = event.toNostrUri(), + uri = baseChannel.toNAddr(), ) } 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 5d4218e43d..e102802be6 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 @@ -127,6 +127,7 @@ import com.vitorpamplona.amethyst.ui.note.types.FileStorageHeaderDisplay import com.vitorpamplona.amethyst.ui.note.types.PictureDisplay import com.vitorpamplona.amethyst.ui.note.types.RenderAppDefinition import com.vitorpamplona.amethyst.ui.note.types.RenderChannelMessage +import com.vitorpamplona.amethyst.ui.note.types.RenderChatMessageEncryptedFile import com.vitorpamplona.amethyst.ui.note.types.RenderEmojiPack import com.vitorpamplona.amethyst.ui.note.types.RenderFhirResource import com.vitorpamplona.amethyst.ui.note.types.RenderGitIssueEvent @@ -177,6 +178,7 @@ import com.vitorpamplona.quartz.nip01Core.tags.addressables.isTaggedAddressableK import com.vitorpamplona.quartz.nip01Core.tags.geohash.getGeoHash import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent import com.vitorpamplona.quartz.nip13Pow.strongPoWOrNull +import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent @@ -564,6 +566,17 @@ private fun FullBleedNoteCompose( ) } else if (noteEvent is ChatMessageRelayListEvent) { DisplayDMRelayList(baseNote, backgroundColor, accountViewModel, nav) + } else if (noteEvent is ChatMessageEncryptedFileHeaderEvent) { + RenderChatMessageEncryptedFile( + baseNote, + false, + canPreview, + 3, + backgroundColor, + editState, + accountViewModel, + nav, + ) } else if (noteEvent is AdvertisedRelayListEvent) { DisplayNIP65RelayList(baseNote, backgroundColor, accountViewModel, nav) } else if (noteEvent is SearchRelayListEvent) { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/EventExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/EventExt.kt index ac8525a35f..4bf2ce9284 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/EventExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/EventExt.kt @@ -25,9 +25,9 @@ import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent -fun Event.toNIP19(): String = +fun Event.toNIP19(relayHint: String? = null): String = if (this is AddressableEvent) { - ATag(kind, pubKey, dTag(), null).toNAddr() + ATag(kind, pubKey, dTag(), relayHint).toNAddr() } else { - NEvent.create(id, pubKey, kind, null) + NEvent.create(id, pubKey, kind, relayHint) } 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 1b430201b5..9cb8319f02 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip21UriScheme/EventExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip21UriScheme/EventExt.kt @@ -24,6 +24,6 @@ import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip19Bech32.toNIP19 -fun Event.toNostrUri(): String = "nostr:${toNIP19()}" +fun Event.toNostrUri(relayHint: String? = null): String = "nostr:${toNIP19(relayHint)}" fun EventHintBundle.toNostrUri(): String = "nostr:${toNEvent()}" From e6cd3a99c50abda63c5fdacc169029cb04ff8476 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 25 Feb 2025 16:43:15 -0500 Subject: [PATCH 46/76] Fixes bug on saving Encrypted files on DMs. Fixes bug when using partial downloads to display videos on DMs. --- .../amethyst/DMDecryptionTest.kt | 62 ++++++++++++++ .../okhttp/EncryptedBlobInterceptor.kt | 78 +++++++++++++----- .../service/okhttp/EncryptionKeyCache.kt | 25 ++++-- .../service/okhttp/HttpClientManager.kt | 3 +- .../amethyst/ui/actions/MediaSaverToDisk.kt | 15 +++- .../ui/components/ZoomableContentDialog.kt | 1 + .../ui/note/types/ChatMessageEncryptedFile.kt | 6 +- quartz/src/androidTest/assets/trouble_video | Bin 0 -> 1277138 bytes .../quartz/nip17Dm/AESGCMTest.kt | 18 ++++ .../quartz/nip04Dm/crypto/AESCBC.kt | 10 +++ .../quartz/nip17Dm/files/encryption/AESGCM.kt | 10 +++ .../nip17Dm/files/encryption/NostrCipher.kt | 2 + 12 files changed, 194 insertions(+), 36 deletions(-) create mode 100644 amethyst/src/androidTest/java/com/vitorpamplona/amethyst/DMDecryptionTest.kt create mode 100644 quartz/src/androidTest/assets/trouble_video diff --git a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/DMDecryptionTest.kt b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/DMDecryptionTest.kt new file mode 100644 index 0000000000..ed80e2f775 --- /dev/null +++ b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/DMDecryptionTest.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.amethyst + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager +import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray +import com.vitorpamplona.quartz.nip17Dm.files.encryption.AESGCM +import junit.framework.TestCase.assertEquals +import okhttp3.Request +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class DMDecryptionTest { + val okHttp = HttpClientManager.getHttpClient(false) + + val url = "https://cdn.satellite.earth/812fd4cf9d4d4b59c141ecd6a6c08c7571b5872237ad6477916cb2d119b5cacd" + val cipher = + AESGCM( + "7b184b3849e161027bab1aac9f2d96eb22bfe02c8dc34cad5d2cc436a64f191d".hexToByteArray(), + "3936923383652e3f0d72bfd40568b435".hexToByteArray(), + ) + val decryptedSize = 1277122 + val expectedMimeType = "video/mp4" + + @Test + fun runDownloadAndDecryptVideo() { + HttpClientManager.addCipherToCache(url, cipher, "video/mp4") + + val request = + Request + .Builder() + .header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}") + .url(url) + .get() + .build() + + okHttp.newCall(request).execute().use { + assertEquals(decryptedSize, it.body.bytes().size) + assertEquals(expectedMimeType, it.body.contentType().toString()) + } + } +} 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 85447bfe17..789e16c1e3 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 @@ -20,51 +20,85 @@ */ package com.vitorpamplona.amethyst.service.okhttp -import android.util.Log import com.vitorpamplona.quartz.nip17Dm.files.encryption.AESGCM -import com.vitorpamplona.quartz.nip17Dm.files.encryption.NostrCipher import okhttp3.Interceptor +import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.Response import okhttp3.ResponseBody.Companion.toResponseBody class EncryptedBlobInterceptor( val cache: EncryptionKeyCache, ) : Interceptor { - fun Response.decrypt(cipher: NostrCipher): Response { + private fun Response.decryptOrNullWithErrorCorrection(info: DecryptInformation): Response? { val body = peekBody(Long.MAX_VALUE) - val decryptedBytes = cipher.decrypt(body.bytes()) - val newBody = decryptedBytes.toResponseBody(body.contentType()) - return newBuilder().body(newBody).build() - } - fun Response.decryptOrNull(cipher: NostrCipher): Response? = - try { - decrypt(cipher) - } catch (e: Exception) { - Log.w("EncryptedBlobInterceptor", "Failed to decrypt", e) - null + // Only tries to decrypt if the content-type is a byte array + if (body.contentType().toString() != "application/octet-stream") { + return null } - private fun Response.decryptOrNullWithErrorCorrection(cipher: NostrCipher): Response? { - return decryptOrNull(cipher) ?: return if (cipher is AESGCM) { - decryptOrNull(cipher.copyUsingUTF8Nonce()) - } else { - null + val bytes = body.bytes() + + // Tries the correct way first + // if it fails, tries to decrypt as UTF8 nonce, which was how + // 0xChat started encrypting + val decrypted = + info.cipher.decryptOrNull(bytes) ?: if (info.cipher is AESGCM) { + info.cipher.copyUsingUTF8Nonce().decryptOrNull(bytes) + } else { + null + } + + if (decrypted == null) { + return null } + + return newBuilder() + .apply { + body( + decrypted.toResponseBody( + info.mimeType?.toMediaTypeOrNull() ?: body.contentType(), + ), + ) + // removes hints that would make the app requrest partial byte arrays + // in videos, which are impossible to decrypt. + removeHeader("accept-ranges") + // Fixes the size of the body array + header("content-length", decrypted.size.toString()) + // Trusts the mimetype from the event is better than the mimetype from the server + info.mimeType?.let { header("content-type", it) } + }.build() } override fun intercept(chain: Interceptor.Chain): Response { val request = chain.request() - val response = chain.proceed(request) + val encryptionKeys = cache.get(request.url.toString()) - val cipher = cache.get(request.url.toString()) ?: return response + // We cannot use Range requests (partial byte arrays) + // in encrypted payloads because we won't be able to + // decrypt partial byte arrays. + val newRequest = + if (encryptionKeys != null) { + request + .newBuilder() + .removeHeader("Range") + .build() + } else { + request + } + + val response = chain.proceed(newRequest) + + if (encryptionKeys == null) { + return response + } if (response.isSuccessful) { - return response.decryptOrNullWithErrorCorrection(cipher) ?: response + return response.decryptOrNullWithErrorCorrection(encryptionKeys) ?: response } else { // Log redirections to be able to use the cipher. response.header("Location")?.let { - cache.add(it, cipher) + cache.add(it, encryptionKeys) } } return response 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 cd0fa1770a..6677491e13 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 @@ -30,16 +30,27 @@ import com.vitorpamplona.quartz.nip17Dm.files.encryption.NostrCipher * This class serves as a key cache to decrypt the body of HTTP calls that need it. */ class EncryptionKeyCache { - val cache = LruCache(100) + val cache = LruCache(100) + + fun add( + url: String?, + decryptInformation: DecryptInformation, + ) { + if (cache.get(url) == null) { + cache.put(url, decryptInformation) + } + } fun add( url: String?, cipher: NostrCipher, - ) { - if (cache.get(url) == null) { - cache.put(url, cipher) - } - } + expectedMimeType: String?, + ) = add(url, DecryptInformation(cipher, expectedMimeType)) - fun get(url: String): NostrCipher? = cache.get(url) + fun get(url: String): DecryptInformation? = cache.get(url) } + +class DecryptInformation( + val cipher: NostrCipher, + val mimeType: String?, +) 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 85f418a1ba..f1ffc9174e 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 @@ -124,5 +124,6 @@ object HttpClientManager { fun addCipherToCache( url: String, cipher: NostrCipher, - ) = cache.add(url, cipher) + expectedMimeType: String?, + ) = cache.add(url, cipher, expectedMimeType) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/MediaSaverToDisk.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/MediaSaverToDisk.kt index 6bf5b480d5..c005df88cc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/MediaSaverToDisk.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/MediaSaverToDisk.kt @@ -58,17 +58,18 @@ object MediaSaverToDisk { if (videoUri != null) { if (!videoUri.startsWith("file")) { downloadAndSave( + url = videoUri, + mimeType = mimeType, context = localContext, forceProxy = forceProxy, - url = videoUri, onSuccess = onSuccess, onError = onError, ) } else { save( - context = localContext, localFile = videoUri.toUri().toFile(), mimeType = mimeType, + context = localContext, onSuccess = onSuccess, onError = onError, ) @@ -83,6 +84,7 @@ object MediaSaverToDisk { */ fun downloadAndSave( url: String, + mimeType: String?, forceProxy: Boolean, context: Context, onSuccess: () -> Any?, @@ -121,9 +123,16 @@ object MediaSaverToDisk { val contentType = response.header("Content-Type") checkNotNull(contentType) { "Can't find out the content type" } + val realType = + if (mimeType != null && contentType == "application/octet-stream") { + mimeType + } else { + contentType + } + saveContentQ( displayName = File(url).nameWithoutExtension, - contentType = contentType, + contentType = realType, contentSource = response.body.source(), contentResolver = context.contentResolver, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentDialog.kt index 17d98c244c..c99dc3cb6d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentDialog.kt @@ -308,6 +308,7 @@ private fun saveMediaToGallery( MediaSaverToDisk.downloadAndSave( content.url, + mimeType = content.mimeType, forceProxy = useTor, localContext, onSuccess = { 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 f3413aff79..fe6c06844f 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 @@ -113,13 +113,13 @@ fun RenderEncryptedFile( val algo = noteEvent.algo() val key = noteEvent.key() val nonce = noteEvent.nonce() + val mimeType = noteEvent.mimeType() if (algo == AESGCM.NAME && key != null && nonce != null) { - HttpClientManager.addCipherToCache(noteEvent.content, AESGCM(key, nonce)) + HttpClientManager.addCipherToCache(noteEvent.content, AESGCM(key, nonce), mimeType) val content by remember(noteEvent) { - val isImage = noteEvent.mimeType()?.startsWith("image/") == true || RichTextParser.isImageUrl(noteEvent.content) - val mimeType = noteEvent.mimeType() + val isImage = mimeType?.startsWith("image/") == true || RichTextParser.isImageUrl(noteEvent.content) mutableStateOf( if (isImage) { diff --git a/quartz/src/androidTest/assets/trouble_video b/quartz/src/androidTest/assets/trouble_video new file mode 100644 index 0000000000000000000000000000000000000000..401d5e1df2391045e8794cba0acc8caedda5c312 GIT binary patch literal 1277138 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 T&agH}9y26yyvVUX*%A7EeZSwolS`6i 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

f#LFSNr;&kuAzbFml=Al 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 z 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

3);&WA2&$7ayPzLXrSB0-xJ2Zd!LS`)yUcJE z7D%H!7Z71(VRT8DB2V;~E$vylw5pwmI)I&T z=Og>!2XS5=elMe1Hl1b7(_`*ElxsDm4dOizTS2d$wOGGW1aaCfqRn(wp?0Waj!EUq z2-Du)wvm$zZ_0Eo-iun^7HABdJ$Bugm845=eLAU*tD^4^>)BYs1n6Ze>Ew(DqK`Z( zhEZ9Ih569fF>E?RoFFti>+HHZs4-FL@t@weWTa!^hv^*vPaJJ;j_jnmci=1YDX)~xcDL1?< z6_gqaI-BiEuokZ1@pzaiD}L!f);J=VD7#|^TnLO?PsHXU5K2%%QSAH(=Z-7j|NReM z78__;prnV;FmV$Wb{GLBHad5)Q#$x6Z5gsAP2JtS>!5pGjP#7FB){;iV7Lv?w&A}<`ltv!wr1Bp09cnf|By2y`TH7$nN&W<;(B;V#( z#K-*7NA_;VH-)|9{8|GXNnrk>o^gzde)g(J^T3Be&4Hs(ZQ-!yM0VOxh{K4+h!ZMX z<$+}a;}CmnPYuBeH~*HXt2P#sTX9u^a>>FY-F@b=iz>kc&oJ^JSIkVR)DmnpqGU8# zsSXW@ZwSBGc~pKP>Y1e*r{+&$0{?$W?60st^_U}!750A8H7|d9UD=OuwsCh{ zCbKX;8t<0s;tHW~H6}pF>5B-vkIBZ-TzyLm|CEL{BX6pNAZW}rbUCuT^}}W_ zZj}xnP?k^4RdK&=q=~qh2!iOct18$7u}6K~-MeCn10)DH@gQ8)STUl@Mx4nJhPV2R zt&>%MwBwtXqW)>G!8P?&5V37h(QFrtz{*f!Jm`P!6A35V>q0>Vz(ta9pAulAKv;V~ zXKke~X0_UrovJnw<%I-ZR$&6S3H(qz_^ePhLn;al($h3}R%BPpho?o9XO$U+puj{< z;~qK?X1k*)Hxk5OlOO0v8%rq~q5ssT|9~%N{fSs`SKaA!FJ0CvfW+Za;zq-UJi?Bj z7ko#Gk@2qWJEgZlX0;sBdyBg zTQ#+ywG)kixymxw#!jHsQv*I9co=#IRp65E2sk;s3DS0rY}#)|*Ar>Oa1Go9F!&%k z!bAu$<@H3PKaLl$1m7Bt@v+*kb7Lyo!pi=lP*IkRKvkrYynM9Z>fGn!)RDLWgz2F6 z;pl%$I>dti66I>rIvo}}r3Y=(V?JfWFNf7$UJ-H2ia?oDJt;!h{LbjMg8hd!06zZA z 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ⅇ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[=f?Xy~9aZxgV6f8cBWZEH5J9{5r3=Qa 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

85xp_ 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 z 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%0AX-cGsRVr&?U5S1 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

)@Mp90Fb6V+#V8?8Dojb|1O<)&Ldj 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
}#@ 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