From df9b764c1d4da04a9edc62a90b2171b5088af3d0 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 19 Nov 2023 17:28:17 -0500 Subject: [PATCH] Massive refactoring to unify internal signer and the Amber signer. --- .../amethyst/ImageUploadTesting.kt | 6 +- .../com/vitorpamplona/amethyst/Amethyst.kt | 10 + .../amethyst/LocalPreferences.kt | 45 +- .../vitorpamplona/amethyst/ServiceManager.kt | 15 +- .../vitorpamplona/amethyst/model/Account.kt | 2881 +++++------------ .../amethyst/model/LocalCache.kt | 4 +- .../com/vitorpamplona/amethyst/model/Note.kt | 251 +- .../com/vitorpamplona/amethyst/model/User.kt | 4 +- .../amethyst/service/ExternalSignerUtils.kt | 311 -- .../amethyst/service/HttpClient.kt | 5 +- .../service/NostrAccountDataSource.kt | 72 +- .../service/NostrDiscoveryDataSource.kt | 64 +- .../amethyst/service/NostrHomeDataSource.kt | 43 +- .../NostrLnZapPaymentResponseDataSource.kt | 14 +- .../NostrSearchEventOrUserDataSource.kt | 12 +- .../amethyst/service/NostrVideoDataSource.kt | 40 +- .../amethyst/service/ZapPaymentHandler.kt | 102 +- .../service/lnurl/LightningAddressResolver.kt | 24 +- .../EventNotificationConsumer.kt | 169 +- .../service/notifications/RegisterAccounts.kt | 50 +- .../vitorpamplona/amethyst/ui/MainActivity.kt | 38 +- .../amethyst/ui/actions/ImageUploader.kt | 126 +- .../amethyst/ui/actions/NewMediaModel.kt | 27 +- .../amethyst/ui/actions/NewPostView.kt | 3 + .../amethyst/ui/actions/NewPostViewModel.kt | 35 +- .../ui/actions/NewUserMetadataViewModel.kt | 2 +- .../amethyst/ui/components/InvoiceRequest.kt | 41 +- .../ui/dal/BookmarkPrivateFeedFilter.kt | 48 +- .../ui/dal/BookmarkPublicFeedFilter.kt | 6 +- .../amethyst/ui/dal/DiscoverChatFeedFilter.kt | 14 +- .../ui/dal/DiscoverCommunityFeedFilter.kt | 19 +- .../amethyst/ui/dal/DiscoverLiveFeedFilter.kt | 17 +- .../ui/dal/HiddenAccountsFeedFilter.kt | 32 +- .../ui/dal/HomeConversationsFeedFilter.kt | 14 +- .../ui/dal/HomeNewThreadFeedFilter.kt | 16 +- .../amethyst/ui/dal/NotificationFeedFilter.kt | 8 +- .../amethyst/ui/dal/ThreadFeedFilter.kt | 5 +- .../amethyst/ui/dal/VideoFeedFilter.kt | 14 +- .../amethyst/ui/navigation/AppTopBar.kt | 10 +- .../amethyst/ui/navigation/DrawerContent.kt | 28 +- .../amethyst/ui/note/ChannelCardCompose.kt | 14 +- .../amethyst/ui/note/ChatroomHeaderCompose.kt | 39 +- .../ui/note/ChatroomMessageCompose.kt | 37 +- .../amethyst/ui/note/NoteCompose.kt | 114 +- .../amethyst/ui/note/NoteQuickActionMenu.kt | 47 +- .../amethyst/ui/note/PollNote.kt | 2 +- .../amethyst/ui/note/PollNoteViewModel.kt | 22 +- .../amethyst/ui/note/ReactionsRow.kt | 31 +- .../amethyst/ui/note/UserProfilePicture.kt | 106 +- .../amethyst/ui/note/ZapNoteCompose.kt | 24 +- .../amethyst/ui/screen/AccountScreen.kt | 74 +- .../ui/screen/AccountStateViewModel.kt | 30 +- .../amethyst/ui/screen/FeedViewModel.kt | 19 +- .../ui/screen/loggedIn/AccountViewModel.kt | 268 +- .../ui/screen/loggedIn/BookmarkListScreen.kt | 16 +- .../ui/screen/loggedIn/DiscoverScreen.kt | 8 +- .../ui/screen/loggedIn/GeoHashScreen.kt | 24 +- .../ui/screen/loggedIn/HashtagScreen.kt | 24 +- .../ui/screen/loggedIn/HiddenUsersScreen.kt | 24 +- .../amethyst/ui/screen/loggedIn/HomeScreen.kt | 7 +- .../ui/screen/loggedIn/LoadRedirectScreen.kt | 67 +- .../amethyst/ui/screen/loggedIn/MainScreen.kt | 4 +- .../ui/screen/loggedIn/NotificationScreen.kt | 5 +- .../ui/screen/loggedIn/ProfileScreen.kt | 36 +- .../ui/screen/loggedIn/ReportNoteDialog.kt | 27 +- .../ui/screen/loggedIn/VideoScreen.kt | 4 +- .../ui/screen/loggedOff/LoginScreen.kt | 100 +- app/src/main/res/values/strings.xml | 4 +- .../benchmark/GiftWrapReceivingBenchmark.kt | 3 +- .../quartz/events/AdvertisedRelayListEvent.kt | 14 +- .../quartz/events/AppDefinitionEvent.kt | 11 + .../quartz/events/AppRecommendationEvent.kt | 11 + .../quartz/events/AudioHeaderEvent.kt | 18 +- .../quartz/events/AudioTrackEvent.kt | 13 +- .../quartz/events/BookmarkListEvent.kt | 193 +- .../quartz/events/CalendarDateSlotEvent.kt | 13 +- .../quartz/events/CalendarEvent.kt | 13 +- .../quartz/events/CalendarRSVPEvent.kt | 13 +- .../quartz/events/CalendarTimeSlotEvent.kt | 13 +- .../quartz/events/ChannelCreateEvent.kt | 36 +- .../quartz/events/ChannelHideMessageEvent.kt | 14 +- .../quartz/events/ChannelMessageEvent.kt | 19 +- .../quartz/events/ChannelMetadataEvent.kt | 38 +- .../quartz/events/ChannelMuteUserEvent.kt | 21 +- .../quartz/events/ChatMessageEvent.kt | 24 +- .../quartz/events/ClassifiedsEvent.kt | 13 +- .../quartz/events/CommunityDefinitionEvent.kt | 13 +- .../events/CommunityPostApprovalEvent.kt | 19 +- .../quartz/events/ContactListEvent.kt | 136 +- .../quartz/events/DeletionEvent.kt | 12 +- .../quartz/events/EmojiPackEvent.kt | 13 +- .../quartz/events/EmojiPackSelectionEvent.kt | 19 +- .../com/vitorpamplona/quartz/events/Event.kt | 8 +- .../quartz/events/EventFactory.kt | 3 + .../quartz/events/FileHeaderEvent.kt | 13 +- .../quartz/events/FileStorageEvent.kt | 28 +- .../quartz/events/FileStorageHeaderEvent.kt | 53 +- .../quartz/events/GeneralListEvent.kt | 139 +- .../quartz/events/GenericRepostEvent.kt | 17 +- .../quartz/events/GiftWrapEvent.kt | 95 +- .../quartz/events/HTTPAuthorizationEvent.kt | 19 +- .../quartz/events/HighlightEvent.kt | 14 +- .../events/LiveActivitiesChatMessageEvent.kt | 19 +- .../quartz/events/LiveActivitiesEvent.kt | 13 +- .../quartz/events/LnZapPaymentRequestEvent.kt | 52 +- .../events/LnZapPaymentResponseEvent.kt | 54 +- .../quartz/events/LnZapPrivateEvent.kt | 13 +- .../quartz/events/LnZapRequestEvent.kt | 230 +- .../quartz/events/LongTextNoteEvent.kt | 15 +- .../quartz/events/MetadataEvent.kt | 17 +- .../quartz/events/MuteListEvent.kt | 83 +- .../quartz/events/NIP24Factory.kt | 162 +- .../vitorpamplona/quartz/events/NNSEvent.kt | 13 +- .../quartz/events/PeopleListEvent.kt | 395 +-- .../quartz/events/PinListEvent.kt | 13 +- .../quartz/events/PollNoteEvent.kt | 19 +- .../quartz/events/PrivateDmEvent.kt | 101 +- .../quartz/events/ReactionEvent.kt | 27 +- .../quartz/events/RecommendRelayEvent.kt | 13 +- .../quartz/events/RelayAuthEvent.kt | 18 +- .../quartz/events/RelaySetEvent.kt | 13 +- .../quartz/events/ReportEvent.kt | 26 +- .../quartz/events/RepostEvent.kt | 17 +- .../quartz/events/SealedGossipEvent.kt | 125 +- .../quartz/events/StatusEvent.kt | 44 +- .../quartz/events/TextNoteEvent.kt | 20 +- .../quartz/signers/ExternalSignerLauncher.kt | 200 ++ .../quartz/signers/NostrSigner.kt | 23 + .../quartz/signers/NostrSignerExternal.kt | 110 + .../quartz/signers/NostrSignerInternal.kt | 218 ++ 130 files changed, 3890 insertions(+), 5046 deletions(-) delete mode 100644 app/src/main/java/com/vitorpamplona/amethyst/service/ExternalSignerUtils.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/signers/ExternalSignerLauncher.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/signers/NostrSigner.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/signers/NostrSignerExternal.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/signers/NostrSignerInternal.kt diff --git a/app/src/androidTest/java/com/vitorpamplona/amethyst/ImageUploadTesting.kt b/app/src/androidTest/java/com/vitorpamplona/amethyst/ImageUploadTesting.kt index fdc3cc836..7b0813c37 100644 --- a/app/src/androidTest/java/com/vitorpamplona/amethyst/ImageUploadTesting.kt +++ b/app/src/androidTest/java/com/vitorpamplona/amethyst/ImageUploadTesting.kt @@ -29,11 +29,7 @@ class ImageUploadTesting { var url: String? = null var error: String? = null - ImageUploader.account = Account( - KeyPair() - ) - - ImageUploader.uploadImage( + ImageUploader(Account(KeyPair())).uploadImage( inputStream, bytes.size.toLong(), "image/gif", diff --git a/app/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt b/app/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt index 82f746fe2..3e15e6e78 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt @@ -9,13 +9,23 @@ import android.util.Log import coil.ImageLoader import coil.disk.DiskCache import com.vitorpamplona.amethyst.service.playback.VideoCache +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel import kotlinx.coroutines.launch import java.io.File import kotlin.time.measureTimedValue class Amethyst : Application() { + val applicationIOScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + + override fun onTerminate() { + super.onTerminate() + applicationIOScope.cancel() + } + val videoCache: VideoCache by lazy { val newCache = VideoCache() newCache.initFileCache(this) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index e06ebfae7..b22fced76 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -29,7 +29,10 @@ import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.events.ContactListEvent import com.vitorpamplona.quartz.events.Event import com.vitorpamplona.quartz.events.LnZapEvent +import com.vitorpamplona.quartz.signers.NostrSignerExternal +import com.vitorpamplona.quartz.signers.NostrSignerInternal import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.withContext import java.io.File import java.util.Locale @@ -164,7 +167,7 @@ object LocalPreferences { val accInfo = AccountInfo( npub, account.isWriteable(), - account.loginWithExternalSigner + account.signer is NostrSignerExternal ) updateCurrentAccount(npub) addAccount(accInfo) @@ -243,16 +246,13 @@ object LocalPreferences { val prefs = encryptedPreferences(account.userProfile().pubkeyNpub()) prefs.edit().apply { - putBoolean(PrefKeys.LOGIN_WITH_EXTERNAL_SIGNER, account.loginWithExternalSigner) - if (account.loginWithExternalSigner) { + putBoolean(PrefKeys.LOGIN_WITH_EXTERNAL_SIGNER, account.signer is NostrSignerExternal) + if (account.signer is NostrSignerExternal) { remove(PrefKeys.NOSTR_PRIVKEY) } else { account.keyPair.privKey?.let { putString(PrefKeys.NOSTR_PRIVKEY, it.toHexKey()) } } account.keyPair.pubKey.let { putString(PrefKeys.NOSTR_PUBKEY, it.toHexKey()) } - putStringSet(PrefKeys.FOLLOWING_CHANNELS, account.followingChannels) - putStringSet(PrefKeys.FOLLOWING_COMMUNITIES, account.followingCommunities) - putStringSet(PrefKeys.HIDDEN_USERS, account.hiddenUsers) putString(PrefKeys.RELAYS, Event.mapper.writeValueAsString(account.localRelays)) putStringSet(PrefKeys.DONT_TRANSLATE_FROM, account.dontTranslateFrom) putString(PrefKeys.LANGUAGE_PREFS, Event.mapper.writeValueAsString(account.languagePreferences)) @@ -261,10 +261,10 @@ object LocalPreferences { putString(PrefKeys.REACTION_CHOICES, Event.mapper.writeValueAsString(account.reactionChoices)) putString(PrefKeys.DEFAULT_ZAPTYPE, account.defaultZapType.name) putString(PrefKeys.DEFAULT_FILE_SERVER, account.defaultFileServer.name) - putString(PrefKeys.DEFAULT_HOME_FOLLOW_LIST, account.defaultHomeFollowList) - putString(PrefKeys.DEFAULT_STORIES_FOLLOW_LIST, account.defaultStoriesFollowList) - putString(PrefKeys.DEFAULT_NOTIFICATION_FOLLOW_LIST, account.defaultNotificationFollowList) - putString(PrefKeys.DEFAULT_DISCOVERY_FOLLOW_LIST, account.defaultDiscoveryFollowList) + putString(PrefKeys.DEFAULT_HOME_FOLLOW_LIST, account.defaultHomeFollowList.value) + putString(PrefKeys.DEFAULT_STORIES_FOLLOW_LIST, account.defaultStoriesFollowList.value) + putString(PrefKeys.DEFAULT_NOTIFICATION_FOLLOW_LIST, account.defaultNotificationFollowList.value) + putString(PrefKeys.DEFAULT_DISCOVERY_FOLLOW_LIST, account.defaultDiscoveryFollowList.value) putString(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER, Event.mapper.writeValueAsString(account.zapPaymentRequest)) putString(PrefKeys.LATEST_CONTACT_LIST, Event.mapper.writeValueAsString(account.backupContactList)) putBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, account.hideDeleteRequestDialog) @@ -382,6 +382,7 @@ object LocalPreferences { val pubKey = getString(PrefKeys.NOSTR_PUBKEY, null) ?: return@with null val loginWithExternalSigner = getBoolean(PrefKeys.LOGIN_WITH_EXTERNAL_SIGNER, false) val privKey = if (loginWithExternalSigner) null else getString(PrefKeys.NOSTR_PRIVKEY, null) + val followingChannels = getStringSet(PrefKeys.FOLLOWING_CHANNELS, null) ?: setOf() val followingCommunities = getStringSet(PrefKeys.FOLLOWING_COMMUNITIES, null) ?: setOf() val hiddenUsers = getStringSet(PrefKeys.HIDDEN_USERS, emptySet()) ?: setOf() @@ -472,11 +473,16 @@ object LocalPreferences { mapOf() } + val keyPair = KeyPair(privKey = privKey?.hexToByteArray(), pubKey = pubKey.hexToByteArray()) + val signer = if (loginWithExternalSigner) { + NostrSignerExternal(pubKey) + } else { + NostrSignerInternal(keyPair) + } + return@with Account( - keyPair = KeyPair(privKey = privKey?.hexToByteArray(), pubKey = pubKey.hexToByteArray()), - followingChannels = followingChannels, - followingCommunities = followingCommunities, - hiddenUsers = hiddenUsers, + keyPair = keyPair, + signer = signer, localRelays = localRelays, dontTranslateFrom = dontTranslateFrom, languagePreferences = languagePreferences, @@ -485,10 +491,10 @@ object LocalPreferences { reactionChoices = reactionChoices, defaultZapType = defaultZapType, defaultFileServer = defaultFileServer, - defaultHomeFollowList = defaultHomeFollowList, - defaultStoriesFollowList = defaultStoriesFollowList, - defaultNotificationFollowList = defaultNotificationFollowList, - defaultDiscoveryFollowList = defaultDiscoveryFollowList, + defaultHomeFollowList = MutableStateFlow(defaultHomeFollowList), + defaultStoriesFollowList = MutableStateFlow(defaultStoriesFollowList), + defaultNotificationFollowList = MutableStateFlow(defaultNotificationFollowList), + defaultDiscoveryFollowList = MutableStateFlow(defaultDiscoveryFollowList), zapPaymentRequest = zapPaymentRequestServer, hideDeleteRequestDialog = hideDeleteRequestDialog, hideBlockAlertDialog = hideBlockAlertDialog, @@ -499,8 +505,7 @@ object LocalPreferences { showSensitiveContent = showSensitiveContent, warnAboutPostsWithReports = warnAboutReports, filterSpamFromStrangers = filterSpam, - lastReadPerRoute = lastReadPerRoute, - loginWithExternalSigner = loginWithExternalSigner + lastReadPerRoute = lastReadPerRoute ) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt b/app/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt index bdcb1f5c4..adadedac2 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt @@ -2,13 +2,13 @@ package com.vitorpamplona.amethyst import android.os.Build import android.util.Log +import androidx.compose.runtime.Stable import coil.Coil import coil.decode.GifDecoder import coil.decode.ImageDecoderDecoder import coil.decode.SvgDecoder import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.service.ExternalSignerUtils import com.vitorpamplona.amethyst.service.HttpClient import com.vitorpamplona.amethyst.service.NostrAccountDataSource import com.vitorpamplona.amethyst.service.NostrChannelDataSource @@ -27,21 +27,19 @@ import com.vitorpamplona.amethyst.service.NostrThreadDataSource import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource import com.vitorpamplona.amethyst.service.NostrVideoDataSource import com.vitorpamplona.amethyst.service.relays.Client -import com.vitorpamplona.amethyst.ui.actions.ImageUploader import com.vitorpamplona.quartz.encoders.decodePublicKeyAsHexOrNull import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch +@Stable class ServiceManager { - var shouldPauseService: Boolean = true // to not open amber in a loop trying to use auth relays and registering for notifications private var isStarted: Boolean = false // to not open amber in a loop trying to use auth relays and registering for notifications private var account: Account? = null private fun start(account: Account) { this.account = account - ExternalSignerUtils.account = account start() } @@ -55,7 +53,7 @@ class ServiceManager { val myAccount = account // Resets Proxy Use - HttpClient.start(account) + HttpClient.start(account?.proxy) OptOutFromFilters.start(account?.warnAboutPostsWithReports ?: true, account?.filterSpamFromStrangers ?: true) Coil.setImageLoader { Amethyst.instance.imageLoaderBuilder().components { @@ -80,7 +78,6 @@ class ServiceManager { NostrChatroomListDataSource.account = myAccount NostrVideoDataSource.account = myAccount NostrDiscoveryDataSource.account = myAccount - ImageUploader.account = myAccount // Notification Elements NostrHomeDataSource.start() @@ -171,10 +168,8 @@ class ServiceManager { } } - fun forceRestartIfItShould() { - if (shouldPauseService) { - forceRestart(null, true, true) - } + fun forceRestart() { + forceRestart(null, true, true) } fun justStart() { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index b3ac9fb02..dc20ca702 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -6,20 +6,21 @@ import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import androidx.core.os.ConfigurationCompat import androidx.lifecycle.LiveData -import androidx.lifecycle.distinctUntilChanged +import androidx.lifecycle.asFlow +import androidx.lifecycle.asLiveData +import androidx.lifecycle.liveData +import androidx.lifecycle.switchMap +import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.OptOutFromFilters -import com.vitorpamplona.amethyst.service.ExternalSignerUtils import com.vitorpamplona.amethyst.service.FileHeader import com.vitorpamplona.amethyst.service.NostrLnZapPaymentResponseDataSource -import com.vitorpamplona.amethyst.service.SignerType +import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.relays.Client import com.vitorpamplona.amethyst.service.relays.Constants import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.Relay import com.vitorpamplona.amethyst.service.relays.RelayPool import com.vitorpamplona.amethyst.ui.components.BundledUpdate -import com.vitorpamplona.amethyst.ui.note.combineWith -import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.crypto.KeyPair import com.vitorpamplona.quartz.encoders.ATag import com.vitorpamplona.quartz.encoders.HexKey @@ -40,10 +41,8 @@ import com.vitorpamplona.quartz.events.Event import com.vitorpamplona.quartz.events.FileHeaderEvent import com.vitorpamplona.quartz.events.FileStorageEvent import com.vitorpamplona.quartz.events.FileStorageHeaderEvent -import com.vitorpamplona.quartz.events.GeneralListEvent import com.vitorpamplona.quartz.events.GenericRepostEvent import com.vitorpamplona.quartz.events.GiftWrapEvent -import com.vitorpamplona.quartz.events.Gossip import com.vitorpamplona.quartz.events.HTTPAuthorizationEvent import com.vitorpamplona.quartz.events.IdentityClaim import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent @@ -66,17 +65,31 @@ import com.vitorpamplona.quartz.events.StatusEvent import com.vitorpamplona.quartz.events.TextNoteEvent import com.vitorpamplona.quartz.events.WrappedEvent import com.vitorpamplona.quartz.events.ZapSplitSetup +import com.vitorpamplona.quartz.signers.NostrSigner +import com.vitorpamplona.quartz.signers.NostrSignerExternal +import com.vitorpamplona.quartz.signers.NostrSignerInternal import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.persistentSetOf import kotlinx.collections.immutable.toImmutableSet import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combineTransform +import kotlinx.coroutines.flow.flattenMerge +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.transformLatest import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeoutOrNull import java.math.BigDecimal import java.net.Proxy import java.util.Locale +import kotlin.coroutines.resume val DefaultChannels = setOf( "25e5c82273a271cb1a840d0060391a0bf4965cafeb029d5ab55350b418953fbb", // -> Anigma's Nostr @@ -103,10 +116,7 @@ val KIND3_FOLLOWS = " All Follows " // This has spaces to avoid mixing with a po @Stable class Account( val keyPair: KeyPair, - - var followingChannels: Set = DefaultChannels, // deprecated - var followingCommunities: Set = setOf(), // deprecated - var hiddenUsers: Set = setOf(), // deprecated + val signer: NostrSigner = NostrSignerInternal(keyPair), var localRelays: Set = Constants.defaultRelays.toSet(), var dontTranslateFrom: Set = getLanguagesSpokenByUser(), @@ -116,10 +126,10 @@ class Account( var reactionChoices: List = DefaultReactions, var defaultZapType: LnZapEvent.ZapType = LnZapEvent.ZapType.PRIVATE, var defaultFileServer: ServersAvailable = ServersAvailable.NOSTR_BUILD, - var defaultHomeFollowList: String = KIND3_FOLLOWS, - var defaultStoriesFollowList: String = GLOBAL_FOLLOWS, - var defaultNotificationFollowList: String = GLOBAL_FOLLOWS, - var defaultDiscoveryFollowList: String = GLOBAL_FOLLOWS, + var defaultHomeFollowList: MutableStateFlow = MutableStateFlow(KIND3_FOLLOWS), + var defaultStoriesFollowList: MutableStateFlow = MutableStateFlow(GLOBAL_FOLLOWS), + var defaultNotificationFollowList: MutableStateFlow = MutableStateFlow(GLOBAL_FOLLOWS), + var defaultDiscoveryFollowList: MutableStateFlow = MutableStateFlow(GLOBAL_FOLLOWS), var zapPaymentRequest: Nip47URI? = null, var hideDeleteRequestDialog: Boolean = false, var hideBlockAlertDialog: Boolean = false, @@ -130,9 +140,11 @@ class Account( var showSensitiveContent: Boolean? = null, var warnAboutPostsWithReports: Boolean = true, var filterSpamFromStrangers: Boolean = true, - var lastReadPerRoute: Map = mapOf(), - var loginWithExternalSigner: Boolean = false + var lastReadPerRoute: Map = mapOf() ) { + // Uses a single scope for the entire application. + val scope = Amethyst.instance.applicationIOScope + var transientHiddenUsers: ImmutableSet = persistentSetOf() // Observers line up here. @@ -140,6 +152,166 @@ class Account( val liveLanguages: AccountLiveData = AccountLiveData(this) val saveable: AccountLiveData = AccountLiveData(this) + @Immutable + data class LiveFollowLists( + val users: ImmutableSet = persistentSetOf(), + val hashtags: ImmutableSet = persistentSetOf(), + val geotags: ImmutableSet = persistentSetOf(), + val communities: ImmutableSet = persistentSetOf() + ) + + @OptIn(ExperimentalCoroutinesApi::class) + val liveKind3Follows: StateFlow by lazy { + userProfile().live().follows.asFlow().transformLatest { + emit( + LiveFollowLists( + userProfile().cachedFollowingKeySet().toImmutableSet(), + userProfile().cachedFollowingTagSet().toImmutableSet(), + userProfile().cachedFollowingGeohashSet().toImmutableSet(), + userProfile().cachedFollowingCommunitiesSet().toImmutableSet() + ) + ) + }.stateIn(scope, SharingStarted.Eagerly, LiveFollowLists()) + } + + @OptIn(ExperimentalCoroutinesApi::class) + private val liveHomeList: StateFlow by lazy { + defaultHomeFollowList.transformLatest { + LocalCache.checkGetOrCreateAddressableNote(it)?.flow()?.metadata?.stateFlow?.let { + emit(it) + } + }.flattenMerge() + .stateIn(scope, SharingStarted.Eagerly, null) + } + + val liveHomeFollowLists: StateFlow by lazy { + combineTransform(defaultHomeFollowList, liveKind3Follows, liveHomeList) { listName, kind3Follows, peopleListFollows -> + if (listName == GLOBAL_FOLLOWS) { + emit(null) + } else if (listName == KIND3_FOLLOWS) { + emit(kind3Follows) + } else { + val result = withTimeoutOrNull(1000) { + suspendCancellableCoroutine { continuation -> + decryptLiveFollows(peopleListFollows) { + continuation.resume(it) + } + } + } + result?.let { + emit(it) + } + } + }.stateIn(scope, SharingStarted.Eagerly, LiveFollowLists()) + } + + @OptIn(ExperimentalCoroutinesApi::class) + private val liveNotificationList: StateFlow by lazy { + defaultNotificationFollowList.transformLatest { + LocalCache.checkGetOrCreateAddressableNote(it)?.flow()?.metadata?.stateFlow?.let { + emit(it) + } + }.flattenMerge() + .stateIn(scope, SharingStarted.Eagerly, null) + } + + val liveNotificationFollowLists: StateFlow by lazy { + combineTransform(defaultNotificationFollowList, liveKind3Follows, liveNotificationList) { listName, kind3Follows, peopleListFollows -> + if (listName == GLOBAL_FOLLOWS) { + emit(null) + } else if (listName == KIND3_FOLLOWS) { + emit(kind3Follows) + } else { + val result = withTimeoutOrNull(1000) { + suspendCancellableCoroutine { continuation -> + decryptLiveFollows(peopleListFollows) { + continuation.resume(it) + } + } + } + result?.let { + emit(it) + } + } + }.stateIn(scope, SharingStarted.Eagerly, LiveFollowLists()) + } + + @OptIn(ExperimentalCoroutinesApi::class) + private val liveStoriesList: StateFlow by lazy { + defaultStoriesFollowList.transformLatest { + LocalCache.checkGetOrCreateAddressableNote(it)?.flow()?.metadata?.stateFlow?.let { + emit(it) + } + }.flattenMerge() + .stateIn(scope, SharingStarted.Eagerly, null) + } + + val liveStoriesFollowLists: StateFlow by lazy { + combineTransform(defaultStoriesFollowList, liveKind3Follows, liveStoriesList) { listName, kind3Follows, peopleListFollows -> + if (listName == GLOBAL_FOLLOWS) { + emit(null) + } else if (listName == KIND3_FOLLOWS) { + emit(kind3Follows) + } else { + val result = withTimeoutOrNull(1000) { + suspendCancellableCoroutine { continuation -> + decryptLiveFollows(peopleListFollows) { + continuation.resume(it) + } + } + } + result?.let { + emit(it) + } + } + }.stateIn(scope, SharingStarted.Eagerly, LiveFollowLists()) + } + + @OptIn(ExperimentalCoroutinesApi::class) + private val liveDiscoveryList: StateFlow by lazy { + defaultDiscoveryFollowList.transformLatest { + LocalCache.checkGetOrCreateAddressableNote(it)?.flow()?.metadata?.stateFlow?.let { + emit(it) + } + }.flattenMerge() + .stateIn(scope, SharingStarted.Eagerly, null) + } + + val liveDiscoveryFollowLists: StateFlow by lazy { + combineTransform(defaultDiscoveryFollowList, liveKind3Follows, liveDiscoveryList) { listName, kind3Follows, peopleListFollows -> + if (listName == GLOBAL_FOLLOWS) { + emit(null) + } else if (listName == KIND3_FOLLOWS) { + emit(kind3Follows) + } else { + val result = withTimeoutOrNull(1000) { + suspendCancellableCoroutine { continuation -> + decryptLiveFollows(peopleListFollows) { + continuation.resume(it) + } + } + } + result?.let { + emit(it) + } + } + }.stateIn(scope, SharingStarted.Eagerly, LiveFollowLists()) + } + + private fun decryptLiveFollows(peopleListFollows: NoteState?, onReady: (LiveFollowLists) -> Unit) { + val listEvent = (peopleListFollows?.note?.event as? PeopleListEvent) + listEvent?.privateTags(signer) { privateTagList -> + onReady( + LiveFollowLists( + users = (listEvent.bookmarkedPeople() + listEvent.filterUsers(privateTagList)).toImmutableSet(), + hashtags = (listEvent.hashtags() + listEvent.filterHashtags(privateTagList)).toImmutableSet(), + geotags = (listEvent.geohashes() + listEvent.filterGeohashes(privateTagList)).toImmutableSet(), + communities = (listEvent.taggedAddresses() + listEvent.filterAddresses(privateTagList)).map { it.toTag() }.toImmutableSet() + ) + ) + } + } + @Immutable data class LiveHiddenUsers( val hiddenUsers: ImmutableSet, @@ -148,62 +320,54 @@ class Account( val showSensitiveContent: Boolean? ) - val liveHiddenUsers: LiveData by lazy { - live.combineWith(getBlockListNote().live().metadata) { localLive, liveMuteListEvent -> - val blockList = liveMuteListEvent?.note?.event as? PeopleListEvent - if (loginWithExternalSigner) { - val id = blockList?.id - if (id != null) { - if (blockList.decryptedContent == null) { - GlobalScope.launch(Dispatchers.IO) { - val content = blockList.content - if (content.isEmpty()) return@launch - ExternalSignerUtils.decryptBlockList( - content, - keyPair.pubKey.toHexKey(), - blockList.id() - ) - blockList.decryptedContent = ExternalSignerUtils.cachedDecryptedContent[blockList.id] - live.invalidateData() - } + val flowHiddenUsers: StateFlow by lazy { + combineTransform(live.asFlow(), getBlockListNote().flow().metadata.stateFlow) { localLive, blockList -> + checkNotInMainThread() - LiveHiddenUsers( - hiddenUsers = persistentSetOf(), - hiddenWords = persistentSetOf(), - spammers = localLive?.account?.transientHiddenUsers ?: persistentSetOf(), - showSensitiveContent = showSensitiveContent - ) - } else { - blockList.decryptedContent = ExternalSignerUtils.cachedDecryptedContent[blockList.id] - val liveBlockedUsers = blockList.publicAndPrivateUsers(blockList.decryptedContent ?: "") - val liveBlockedWords = blockList.publicAndPrivateWords(blockList.decryptedContent ?: "") - LiveHiddenUsers( - hiddenUsers = liveBlockedUsers, - hiddenWords = liveBlockedWords, - spammers = localLive?.account?.transientHiddenUsers ?: persistentSetOf(), - showSensitiveContent = showSensitiveContent - ) + val result = withTimeoutOrNull(1000) { + suspendCancellableCoroutine { continuation -> + (blockList.note.event as? PeopleListEvent)?.publicAndPrivateUsersAndWords(signer) { + continuation.resume(it) } - } else { - LiveHiddenUsers( - hiddenUsers = persistentSetOf(), - hiddenWords = persistentSetOf(), - spammers = localLive?.account?.transientHiddenUsers - ?: persistentSetOf(), - showSensitiveContent = showSensitiveContent - ) } - } else { - val liveBlockedUsers = blockList?.publicAndPrivateUsers(keyPair.privKey) - val liveBlockedWords = blockList?.publicAndPrivateWords(keyPair.privKey) - LiveHiddenUsers( - hiddenUsers = liveBlockedUsers ?: persistentSetOf(), - hiddenWords = liveBlockedWords ?: persistentSetOf(), - spammers = localLive?.account?.transientHiddenUsers ?: persistentSetOf(), - showSensitiveContent = showSensitiveContent + } + + result?.let { + emit( + LiveHiddenUsers( + hiddenUsers = it.users, + hiddenWords = it.words, + spammers = localLive.account.transientHiddenUsers, + showSensitiveContent = localLive.account.showSensitiveContent + ) ) } - }.distinctUntilChanged() + }.stateIn( + scope, + SharingStarted.Eagerly, + LiveHiddenUsers( + hiddenUsers = persistentSetOf(), + hiddenWords = persistentSetOf(), + spammers = transientHiddenUsers, + showSensitiveContent = showSensitiveContent + ) + ) + } + + val liveHiddenUsers = flowHiddenUsers.asLiveData() + + val decryptBookmarks: LiveData by lazy { + userProfile().live().innerBookmarks.switchMap { userState -> + liveData(Dispatchers.IO) { + userState.user.latestBookmarkList?.privateTags(signer) { + scope.launch(Dispatchers.IO) { + userState.user.latestBookmarkList?.let { + emit(it) + } + } + } + } + } } var userProfileCache: User? = null @@ -228,72 +392,47 @@ class Account( } fun isWriteable(): Boolean { - return keyPair.privKey != null + return keyPair.privKey != null || signer is NostrSignerExternal } fun sendNewRelayList(relays: Map) { - if (!isWriteable() && !loginWithExternalSigner) return + if (!isWriteable()) return val contactList = userProfile().latestContactList if (contactList != null && contactList.tags.isNotEmpty()) { - var event = ContactListEvent.updateRelayList( + val event = ContactListEvent.updateRelayList( earlierVersion = contactList, relayUse = relays, - keyPair = keyPair - ) - - if (loginWithExternalSigner) { - ExternalSignerUtils.openSigner(event) - val content = ExternalSignerUtils.content[event.id] ?: "" - if (content.isBlank()) { - return - } - event = ContactListEvent.create(event, content) + signer = signer + ) { + Client.send(it) + LocalCache.justConsume(it, null) } - - Client.send(event) - LocalCache.consume(event) } else { - var event = ContactListEvent.createFromScratch( + val event = ContactListEvent.createFromScratch( followUsers = listOf(), followTags = listOf(), followGeohashes = listOf(), followCommunities = listOf(), followEvents = DefaultChannels.toList(), relayUse = relays, - keyPair = keyPair - ) - - if (loginWithExternalSigner) { - ExternalSignerUtils.openSigner(event) - val content = ExternalSignerUtils.content[event.id] - if (content.isBlank()) { - return - } - event = ContactListEvent.create(event, content) + signer = signer + ) { + // Keep this local to avoid erasing a good contact list. + // Client.send(it) + LocalCache.justConsume(it, null) } - - // Keep this local to avoid erasing a good contact list. - // Client.send(event) - LocalCache.consume(event) } } - fun sendNewUserMetadata(toString: String, identities: List) { - if (!isWriteable() && !loginWithExternalSigner) return + suspend fun sendNewUserMetadata(toString: String, identities: List) { + if (!isWriteable()) return - var event = MetadataEvent.create(toString, identities, keyPair.pubKey.toHexKey(), keyPair.privKey) - if (loginWithExternalSigner) { - ExternalSignerUtils.openSigner(event) - val content = ExternalSignerUtils.content[event.id] - if (content.isBlank()) { - return - } - event = MetadataEvent.create(event, content) + MetadataEvent.create(toString, identities, signer) { + Client.send(it) + LocalCache.justConsume(it, null) } - Client.send(event) - LocalCache.consume(event) return } @@ -314,8 +453,8 @@ class Account( return note.hasReacted(userProfile(), reaction) } - fun reactTo(note: Note, reaction: String) { - if (!isWriteable() && !loginWithExternalSigner) return + suspend fun reactTo(note: Note, reaction: String) { + if (!isWriteable()) return if (hasReacted(note, reaction)) { // has already liked this note @@ -330,51 +469,13 @@ class Account( val emojiUrl = EmojiUrl.decode(reaction) if (emojiUrl != null) { note.event?.let { - if (loginWithExternalSigner) { - val senderPublicKey = keyPair.pubKey.toHexKey() - - var senderReaction = ReactionEvent.create( - emojiUrl, - it, - keyPair - ) - - ExternalSignerUtils.openSigner(senderReaction) - val reactionContent = ExternalSignerUtils.content[event.id] - if (reactionContent.isBlank()) return - senderReaction = ReactionEvent.create(senderReaction, reactionContent) - - val giftWraps = users.plus(senderPublicKey).map { - val gossip = Gossip.create(senderReaction) - val content = Gossip.toJson(gossip) - ExternalSignerUtils.encrypt(content, it, gossip.id!!, SignerType.NIP44_ENCRYPT) - val encryptedContent = ExternalSignerUtils.content[gossip.id] - if (encryptedContent.isBlank()) return - - var sealedEvent = SealedGossipEvent.create( - encryptedContent = encryptedContent, - pubKey = senderPublicKey - ) - ExternalSignerUtils.openSigner(sealedEvent) - val eventContent = ExternalSignerUtils.content[sealedEvent.id] ?: "" - if (eventContent.isBlank()) return - sealedEvent = SealedGossipEvent.create(sealedEvent, eventContent) - - GiftWrapEvent.create( - event = sealedEvent, - recipientPubKey = it - ) - } - - broadcastPrivately(NIP24Factory.Result(senderReaction, giftWraps)) - } else { - val giftWraps = NIP24Factory().createReactionWithinGroup( - emojiUrl = emojiUrl, - originalNote = it, - to = users, - from = keyPair - ) - broadcastPrivately(giftWraps) + NIP24Factory().createReactionWithinGroup( + emojiUrl = emojiUrl, + originalNote = it, + to = users, + signer = signer + ) { + broadcastPrivately(it) } } @@ -383,53 +484,13 @@ class Account( } note.event?.let { - if (loginWithExternalSigner) { - val senderPublicKey = keyPair.pubKey.toHexKey() - - var senderReaction = ReactionEvent.create( - reaction, - it, - keyPair - ) - - ExternalSignerUtils.openSigner(senderReaction) - val reactionContent = ExternalSignerUtils.content[senderReaction.id] ?: "" - if (reactionContent.isBlank()) return - senderReaction = ReactionEvent.create(senderReaction, reactionContent) - - val newUsers = users.plus(senderPublicKey) - newUsers.forEach { - val gossip = Gossip.create(senderReaction) - val content = Gossip.toJson(gossip) - ExternalSignerUtils.encrypt(content, it, gossip.id!!, SignerType.NIP44_ENCRYPT) - val encryptedContent = ExternalSignerUtils.content[gossip.id] - if (encryptedContent.isBlank()) return - - var sealedEvent = SealedGossipEvent.create( - encryptedContent = encryptedContent, - pubKey = senderPublicKey - ) - ExternalSignerUtils.openSigner(sealedEvent) - val sealedContent = ExternalSignerUtils.content[sealedEvent.id] ?: "" - if (sealedContent.isBlank()) return - sealedEvent = SealedGossipEvent.create(sealedEvent, sealedContent) - - val giftWraps = GiftWrapEvent.create( - event = sealedEvent, - recipientPubKey = it - ) - - broadcastPrivately(NIP24Factory.Result(senderReaction, listOf(giftWraps))) - } - } else { - val giftWraps = NIP24Factory().createReactionWithinGroup( - content = reaction, - originalNote = it, - to = users, - from = keyPair - ) - - broadcastPrivately(giftWraps) + NIP24Factory().createReactionWithinGroup( + content = reaction, + originalNote = it, + to = users, + signer = signer + ) { + broadcastPrivately(it) } } return @@ -438,17 +499,10 @@ class Account( val emojiUrl = EmojiUrl.decode(reaction) if (emojiUrl != null) { note.event?.let { - var event = ReactionEvent.create(emojiUrl, it, keyPair) - if (loginWithExternalSigner) { - ExternalSignerUtils.openSigner(event) - val content = ExternalSignerUtils.content[event.id] ?: "" - if (content.isBlank()) { - return - } - event = ReactionEvent.create(event, content) + ReactionEvent.create(emojiUrl, it, signer) { + Client.send(it) + LocalCache.consume(it) } - Client.send(event) - LocalCache.consume(event) } return @@ -456,89 +510,30 @@ class Account( } note.event?.let { - var event = ReactionEvent.create(reaction, it, keyPair) - if (loginWithExternalSigner) { - ExternalSignerUtils.openSigner(event) - val content = ExternalSignerUtils.content[event.id] ?: "" - if (content.isBlank()) { - return - } - event = ReactionEvent.create(event, content) + ReactionEvent.create(reaction, it, signer) { + Client.send(it) + LocalCache.consume(it) } - Client.send(event) - LocalCache.consume(event) } } } - fun createZapRequestFor(note: Note, pollOption: Int?, message: String = "", zapType: LnZapEvent.ZapType, toUser: User?): LnZapRequestEvent? { - if (!isWriteable() && !loginWithExternalSigner) return null + fun createZapRequestFor(note: Note, pollOption: Int?, message: String = "", zapType: LnZapEvent.ZapType, toUser: User?, onReady: (LnZapRequestEvent) -> Unit) { + if (!isWriteable()) return note.event?.let { event -> - if (loginWithExternalSigner) { - when (zapType) { - LnZapEvent.ZapType.ANONYMOUS -> { - return LnZapRequestEvent.createAnonymous( - event, - userProfile().latestContactList?.relays()?.keys?.ifEmpty { null } - ?: localRelays.map { it.url }.toSet(), - pollOption, - message, - toUser?.pubkeyHex - ) - } - LnZapEvent.ZapType.PUBLIC -> { - val unsignedEvent = LnZapRequestEvent.createPublic( - event, - userProfile().latestContactList?.relays()?.keys?.ifEmpty { null } - ?: localRelays.map { it.url }.toSet(), - keyPair.pubKey.toHexKey(), - pollOption, - message, - toUser?.pubkeyHex - ) - ExternalSignerUtils.openSigner(unsignedEvent) - val content = ExternalSignerUtils.content[unsignedEvent.id] ?: "" - if (content.isBlank()) return null - - return LnZapRequestEvent.create( - unsignedEvent, - content - ) - } - - LnZapEvent.ZapType.PRIVATE -> { - val unsignedEvent = LnZapRequestEvent.createPrivateZap( - event, - userProfile().latestContactList?.relays()?.keys?.ifEmpty { null } - ?: localRelays.map { it.url }.toSet(), - keyPair.pubKey.toHexKey(), - pollOption, - message, - toUser?.pubkeyHex - ) - ExternalSignerUtils.openSigner(unsignedEvent, "event") - val content = ExternalSignerUtils.content[unsignedEvent.id] ?: "" - if (content.isBlank()) return null - - return Event.fromJson(content) as LnZapRequestEvent - } - else -> null - } - } else { - return LnZapRequestEvent.create( - event, - userProfile().latestContactList?.relays()?.keys?.ifEmpty { null } - ?: localRelays.map { it.url }.toSet(), - keyPair.privKey!!, - pollOption, - message, - zapType, - toUser?.pubkeyHex - ) - } + LnZapRequestEvent.create( + event, + userProfile().latestContactList?.relays()?.keys?.ifEmpty { null } + ?: localRelays.map { it.url }.toSet(), + signer, + pollOption, + message, + zapType, + toUser?.pubkeyHex, + onReady = onReady + ) } - return null } fun hasWalletConnectSetup(): Boolean { @@ -546,138 +541,71 @@ class Account( } fun isNIP47Author(pubkeyHex: String?): Boolean { - val privKey = zapPaymentRequest?.secret?.hexToByteArray() ?: keyPair.privKey - - if (privKey == null && !loginWithExternalSigner) return false - - if (privKey != null) { - val pubKey = CryptoUtils.pubkeyCreate(privKey).toHexKey() - return (pubKey == pubkeyHex) - } - - return (keyPair.pubKey.toHexKey() == pubkeyHex) + return (getNIP47Signer().pubKey == pubkeyHex) } - fun decryptZapPaymentResponseEvent(zapResponseEvent: LnZapPaymentResponseEvent): Response? { - val myNip47 = zapPaymentRequest ?: return null - - val privKey = myNip47.secret?.hexToByteArray() ?: keyPair.privKey - val pubKey = myNip47.pubKeyHex.hexToByteArray() - - if (privKey == null && !loginWithExternalSigner) return null - - if (privKey != null) return zapResponseEvent.response(privKey, pubKey) - - ExternalSignerUtils.decrypt(zapResponseEvent.content, pubKey.toHexKey(), zapResponseEvent.id) - val decryptedContent = ExternalSignerUtils.content[zapResponseEvent.id] ?: "" - if (decryptedContent.isBlank()) return null - return zapResponseEvent.response(decryptedContent) + fun getNIP47Signer(): NostrSigner { + return zapPaymentRequest?.secret?.hexToByteArray()?.let { NostrSignerInternal(KeyPair(it)) } ?: signer } - fun calculateIfNoteWasZappedByAccount(zappedNote: Note?): Boolean { - return zappedNote?.isZappedBy(userProfile(), this) == true + fun decryptZapPaymentResponseEvent(zapResponseEvent: LnZapPaymentResponseEvent, onReady: (Response) -> Unit) { + val myNip47 = zapPaymentRequest ?: return + + val signer = myNip47.secret?.hexToByteArray()?.let { NostrSignerInternal(KeyPair(it)) } ?: signer + + zapResponseEvent.response(signer, onReady) } - fun calculateZappedAmount(zappedNote: Note?): BigDecimal { - val privKey = zapPaymentRequest?.secret?.hexToByteArray() ?: keyPair.privKey - val pubKey = zapPaymentRequest?.pubKeyHex?.hexToByteArray() - return zappedNote?.zappedAmountWithNWCPayments(privKey, pubKey) ?: BigDecimal.ZERO + fun calculateIfNoteWasZappedByAccount(zappedNote: Note?, onWasZapped: () -> Unit) { + zappedNote?.isZappedBy(userProfile(), this, onWasZapped) + } + + fun calculateZappedAmount(zappedNote: Note?, onReady: (BigDecimal) -> Unit) { + zappedNote?.zappedAmountWithNWCPayments(getNIP47Signer(), onReady) } fun sendZapPaymentRequestFor(bolt11: String, zappedNote: Note?, onResponse: (Response?) -> Unit) { - if (!isWriteable() && !loginWithExternalSigner) return + if (!isWriteable()) return zapPaymentRequest?.let { nip47 -> - val privateKey = if (loginWithExternalSigner) nip47.secret?.hexToByteArray() else nip47.secret?.hexToByteArray() ?: keyPair.privKey - if (privateKey == null) return - val event = LnZapPaymentRequestEvent.create(bolt11, nip47.pubKeyHex, privateKey) + val signer = nip47.secret?.hexToByteArray()?.let { NostrSignerInternal(KeyPair(it)) } ?: signer - val wcListener = NostrLnZapPaymentResponseDataSource( - fromServiceHex = nip47.pubKeyHex, - toUserHex = event.pubKey, - replyingToHex = event.id, - authSigningKey = privateKey - ) - wcListener.start() + LnZapPaymentRequestEvent.create(bolt11, nip47.pubKeyHex, signer) { event -> + val wcListener = NostrLnZapPaymentResponseDataSource( + fromServiceHex = nip47.pubKeyHex, + toUserHex = event.pubKey, + replyingToHex = event.id, + authSigner = signer + ) + wcListener.start() - LocalCache.consume(event, zappedNote) { - // After the response is received. - val privKey = nip47.secret?.hexToByteArray() - if (privKey != null) { - onResponse(it.response(privKey, nip47.pubKeyHex.hexToByteArray())) + LocalCache.consume(event, zappedNote) { + it.response(signer) { + onResponse(it) + } } - } - Client.send(event, nip47.relayUri, wcListener.feedTypes) { - wcListener.destroy() + Client.send(event, nip47.relayUri, wcListener.feedTypes) { + wcListener.destroy() + } } } } - fun createZapRequestFor(user: User): LnZapRequestEvent? { - return createZapRequestFor(user) + fun createZapRequestFor(userPubKeyHex: String, message: String = "", zapType: LnZapEvent.ZapType, onReady: (LnZapRequestEvent) -> Unit) { + LnZapRequestEvent.create( + userPubKeyHex, + userProfile().latestContactList?.relays()?.keys?.ifEmpty { null } + ?: localRelays.map { it.url }.toSet(), + signer, + message, + zapType, + onReady = onReady + ) } - fun createZapRequestFor(userPubKeyHex: String, message: String = "", zapType: LnZapEvent.ZapType): LnZapRequestEvent? { - if (!isWriteable() && !loginWithExternalSigner) return null - if (loginWithExternalSigner) { - return when (zapType) { - LnZapEvent.ZapType.ANONYMOUS -> { - return LnZapRequestEvent.createAnonymous( - userPubKeyHex, - userProfile().latestContactList?.relays()?.keys?.ifEmpty { null } - ?: localRelays.map { it.url }.toSet(), - message - ) - } - LnZapEvent.ZapType.PUBLIC -> { - val unsignedEvent = LnZapRequestEvent.createPublic( - userPubKeyHex, - userProfile().latestContactList?.relays()?.keys?.ifEmpty { null } - ?: localRelays.map { it.url }.toSet(), - keyPair.pubKey.toHexKey(), - message - ) - ExternalSignerUtils.openSigner(unsignedEvent) - val content = ExternalSignerUtils.content[unsignedEvent.id] ?: "" - if (content.isBlank()) return null - - return LnZapRequestEvent.create( - unsignedEvent, - content - ) - } - - LnZapEvent.ZapType.PRIVATE -> { - val unsignedEvent = LnZapRequestEvent.createPrivateZap( - userPubKeyHex, - userProfile().latestContactList?.relays()?.keys?.ifEmpty { null } - ?: localRelays.map { it.url }.toSet(), - keyPair.pubKey.toHexKey(), - message - ) - ExternalSignerUtils.openSigner(unsignedEvent, "event") - val content = ExternalSignerUtils.content[unsignedEvent.id] ?: "" - if (content.isBlank()) return null - - return Event.fromJson(content) as LnZapRequestEvent - } - else -> null - } - } else { - return LnZapRequestEvent.create( - userPubKeyHex, - userProfile().latestContactList?.relays()?.keys?.ifEmpty { null } - ?: localRelays.map { it.url }.toSet(), - keyPair.privKey!!, - message, - zapType - ) - } - } - - fun report(note: Note, type: ReportEvent.ReportType, content: String = "") { - if (!isWriteable() && !loginWithExternalSigner) return + suspend fun report(note: Note, type: ReportEvent.ReportType, content: String = "") { + if (!isWriteable()) return if (note.hasReacted(userProfile(), "⚠️")) { // has already liked this note @@ -685,116 +613,59 @@ class Account( } note.event?.let { - var event = ReactionEvent.createWarning(it, keyPair) - if (loginWithExternalSigner) { - ExternalSignerUtils.openSigner(event) - val eventContent = ExternalSignerUtils.content[event.id] ?: "" - if (eventContent.isBlank()) return - event = ReactionEvent( - event.id, - event.pubKey, - event.createdAt, - event.tags, - event.content, - eventContent - ) + ReactionEvent.createWarning(it, signer) { + Client.send(it) + LocalCache.justConsume(it, null) } - Client.send(event) - LocalCache.consume(event) } note.event?.let { - var event = ReportEvent.create(it, type, keyPair, content = content) - if (loginWithExternalSigner) { - ExternalSignerUtils.openSigner(event) - val eventContent = ExternalSignerUtils.content[event.id] ?: "" - if (eventContent.isBlank()) return - event = ReportEvent( - event.id, - event.pubKey, - event.createdAt, - event.tags, - event.content, - eventContent - ) + ReportEvent.create(it, type, signer, content) { + Client.send(it) + LocalCache.justConsume(it, null) } - Client.send(event) - LocalCache.consume(event, null) } } - fun report(user: User, type: ReportEvent.ReportType) { - if (!isWriteable() && !loginWithExternalSigner) return + suspend fun report(user: User, type: ReportEvent.ReportType) { + if (!isWriteable()) return if (user.hasReport(userProfile(), type)) { // has already reported this note return } - var event = ReportEvent.create(user.pubkeyHex, type, keyPair) - if (loginWithExternalSigner) { - ExternalSignerUtils.openSigner(event) - val eventContent = ExternalSignerUtils.content[event.id] ?: "" - if (eventContent.isBlank()) return - event = ReportEvent( - event.id, - event.pubKey, - event.createdAt, - event.tags, - event.content, - eventContent - ) + ReportEvent.create(user.pubkeyHex, type, signer) { + Client.send(it) + LocalCache.justConsume(it, null) } - Client.send(event) - LocalCache.consume(event, null) } - fun delete(note: Note) { + suspend fun delete(note: Note) { return delete(listOf(note)) } - fun delete(notes: List) { - if (!isWriteable() && !loginWithExternalSigner) return + suspend fun delete(notes: List) { + if (!isWriteable()) return val myNotes = notes.filter { it.author == userProfile() }.map { it.idHex } if (myNotes.isNotEmpty()) { - var event = DeletionEvent.create(myNotes, keyPair) - if (loginWithExternalSigner) { - ExternalSignerUtils.openSigner(event) - val eventContent = ExternalSignerUtils.content[event.id] ?: "" - if (eventContent.isBlank()) return - event = DeletionEvent( - event.id, - event.pubKey, - event.createdAt, - event.tags, - event.content, - eventContent - ) + DeletionEvent.create(myNotes, signer) { + Client.send(it) + LocalCache.justConsume(it, null) } - Client.send(event) - LocalCache.consume(event) } } - fun createHTTPAuthorization(url: String, method: String, body: String? = null): HTTPAuthorizationEvent? { - if (!isWriteable() && !loginWithExternalSigner) return null + fun createHTTPAuthorization(url: String, method: String, body: String? = null, onReady: (HTTPAuthorizationEvent) -> Unit) { + if (!isWriteable()) return - var event = HTTPAuthorizationEvent.create(url, method, body, keyPair) - if (loginWithExternalSigner) { - ExternalSignerUtils.openSigner(event) - val eventContent = ExternalSignerUtils.content[event.id] ?: "" - if (eventContent.isBlank()) { - return null - } - event = HTTPAuthorizationEvent.create(event, eventContent) - } - return event + HTTPAuthorizationEvent.create(url, method, body, signer, onReady = onReady) } - fun boost(note: Note) { - if (!isWriteable() && !loginWithExternalSigner) return + suspend fun boost(note: Note) { + if (!isWriteable()) return if (note.hasBoostedInTheLast5Minutes(userProfile())) { // has already bosted in the past 5mins @@ -803,39 +674,15 @@ class Account( note.event?.let { if (it.kind() == 1) { - var event = RepostEvent.create(it, keyPair) - if (loginWithExternalSigner) { - ExternalSignerUtils.openSigner(event) - val eventContent = ExternalSignerUtils.content[event.id] ?: "" - if (eventContent.isBlank()) return - event = RepostEvent( - event.id, - event.pubKey, - event.createdAt, - event.tags, - event.content, - eventContent - ) + RepostEvent.create(it, signer) { + Client.send(it) + LocalCache.justConsume(it, null) } - Client.send(event) - LocalCache.consume(event) } else { - var event = GenericRepostEvent.create(it, keyPair) - if (loginWithExternalSigner) { - ExternalSignerUtils.openSigner(event) - val eventContent = ExternalSignerUtils.content[event.id] ?: "" - if (eventContent.isBlank()) return - event = GenericRepostEvent( - event.id, - event.pubKey, - event.createdAt, - event.tags, - event.content, - eventContent - ) + GenericRepostEvent.create(it, signer) { + Client.send(it) + LocalCache.justConsume(it, null) } - Client.send(event) - LocalCache.consume(event) } } } @@ -852,56 +699,16 @@ class Account( } } - private fun migrateCommunitiesAndChannelsIfNeeded(latestContactList: ContactListEvent?): ContactListEvent? { - if (latestContactList == null) return latestContactList + fun follow(user: User) { + if (!isWriteable()) return - var returningContactList: ContactListEvent = latestContactList + val contactList = userProfile().latestContactList - if (followingCommunities.isNotEmpty()) { - followingCommunities.forEach { - ATag.parse(it, null)?.let { - if (loginWithExternalSigner) { - val unsignedEvent = ContactListEvent.followAddressableEvent( - returningContactList, - it, - keyPair - ) - ExternalSignerUtils.openSigner(unsignedEvent) - val eventContent = ExternalSignerUtils.content[unsignedEvent.id] ?: "" - returningContactList = if (eventContent.isBlank()) { - latestContactList - } else { - ContactListEvent.create(unsignedEvent, eventContent) - } - } else { - returningContactList = ContactListEvent.followAddressableEvent( - returningContactList, - it, - keyPair - ) - } - } + if (contactList != null) { + ContactListEvent.followUser(contactList, user.pubkeyHex, signer) { + Client.send(it) + LocalCache.justConsume(it, null) } - followingCommunities = emptySet() - } - - if (followingChannels.isNotEmpty()) { - followingChannels.forEach { - returningContactList = ContactListEvent.followEvent(returningContactList, it, keyPair) - } - followingChannels = emptySet() - } - - return returningContactList - } - - suspend fun follow(user: User) { - if (!isWriteable() && !loginWithExternalSigner) return - - val contactList = migrateCommunitiesAndChannelsIfNeeded(userProfile().latestContactList) - - var event = if (contactList != null) { - ContactListEvent.followUser(contactList, user.pubkeyHex, keyPair) } else { ContactListEvent.createFromScratch( followUsers = listOf(Contact(user.pubkeyHex, null)), @@ -910,30 +717,24 @@ class Account( followCommunities = emptyList(), followEvents = DefaultChannels.toList(), relayUse = Constants.defaultRelays.associate { it.url to ContactListEvent.ReadWrite(it.read, it.write) }, - keyPair = keyPair - ) - } - - if (loginWithExternalSigner) { - ExternalSignerUtils.openSigner(event) - val eventContent = ExternalSignerUtils.content[event.id] ?: "" - if (eventContent.isBlank()) { - return + signer = signer + ) { + Client.send(it) + LocalCache.justConsume(it, null) } - event = ContactListEvent.create(event, eventContent) } - - Client.send(event) - LocalCache.consume(event) } fun follow(channel: Channel) { - if (!isWriteable() && !loginWithExternalSigner) return + if (!isWriteable()) return - val contactList = migrateCommunitiesAndChannelsIfNeeded(userProfile().latestContactList) + val contactList = userProfile().latestContactList - var event = if (contactList != null) { - ContactListEvent.followEvent(contactList, channel.idHex, keyPair) + if (contactList != null) { + ContactListEvent.followEvent(contactList, channel.idHex, signer) { + Client.send(it) + LocalCache.justConsume(it, null) + } } else { ContactListEvent.createFromScratch( followUsers = emptyList(), @@ -942,30 +743,24 @@ class Account( followCommunities = emptyList(), followEvents = DefaultChannels.toList().plus(channel.idHex), relayUse = Constants.defaultRelays.associate { it.url to ContactListEvent.ReadWrite(it.read, it.write) }, - keyPair = keyPair - ) - } - - if (loginWithExternalSigner) { - ExternalSignerUtils.openSigner(event) - val eventContent = ExternalSignerUtils.content[event.id] ?: "" - if (eventContent.isBlank()) { - return + signer = signer + ) { + Client.send(it) + LocalCache.justConsume(it, null) } - event = ContactListEvent.create(event, eventContent) } - - Client.send(event) - LocalCache.consume(event) } fun follow(community: AddressableNote) { - if (!isWriteable() && !loginWithExternalSigner) return + if (!isWriteable()) return - val contactList = migrateCommunitiesAndChannelsIfNeeded(userProfile().latestContactList) + val contactList = userProfile().latestContactList - var event = if (contactList != null) { - ContactListEvent.followAddressableEvent(contactList, community.address, keyPair) + if (contactList != null) { + ContactListEvent.followAddressableEvent(contactList, community.address, signer) { + Client.send(it) + LocalCache.justConsume(it, null) + } } else { val relays = Constants.defaultRelays.associate { it.url to ContactListEvent.ReadWrite(it.read, it.write) } ContactListEvent.createFromScratch( @@ -975,34 +770,28 @@ class Account( followCommunities = listOf(community.address), followEvents = DefaultChannels.toList(), relayUse = relays, - keyPair = keyPair - ) - } - - if (loginWithExternalSigner) { - ExternalSignerUtils.openSigner(event) - val eventContent = ExternalSignerUtils.content[event.id] ?: "" - if (eventContent.isBlank()) { - return + signer = signer + ) { + Client.send(it) + LocalCache.justConsume(it, null) } - event = ContactListEvent.create(event, eventContent) } - - Client.send(event) - LocalCache.consume(event) } fun followHashtag(tag: String) { - if (!isWriteable() && !loginWithExternalSigner) return + if (!isWriteable()) return - val contactList = migrateCommunitiesAndChannelsIfNeeded(userProfile().latestContactList) + val contactList = userProfile().latestContactList - var event = if (contactList != null) { + if (contactList != null) { ContactListEvent.followHashtag( contactList, tag, - keyPair - ) + signer + ) { + Client.send(it) + LocalCache.justConsume(it, null) + } } else { ContactListEvent.createFromScratch( followUsers = emptyList(), @@ -1011,33 +800,25 @@ class Account( followCommunities = emptyList(), followEvents = DefaultChannels.toList(), relayUse = Constants.defaultRelays.associate { it.url to ContactListEvent.ReadWrite(it.read, it.write) }, - keyPair = keyPair - ) - } - - if (loginWithExternalSigner) { - ExternalSignerUtils.openSigner(event) - val eventContent = ExternalSignerUtils.content[event.id] ?: "" - if (eventContent.isBlank()) { - return + signer = signer + ) { + Client.send(it) + LocalCache.justConsume(it, null) } - event = ContactListEvent.create(event, eventContent) } - - Client.send(event) - LocalCache.consume(event) } fun followGeohash(geohash: String) { - if (!isWriteable() && !loginWithExternalSigner) return + if (!isWriteable()) return - val contactList = migrateCommunitiesAndChannelsIfNeeded(userProfile().latestContactList) + val contactList = userProfile().latestContactList - var event = if (contactList != null) { + if (contactList != null) { ContactListEvent.followGeohash( contactList, geohash, - keyPair + signer, + onReady = this::onNewEventCreated ) } else { ContactListEvent.createFromScratch( @@ -1047,176 +828,101 @@ class Account( followCommunities = emptyList(), followEvents = DefaultChannels.toList(), relayUse = Constants.defaultRelays.associate { it.url to ContactListEvent.ReadWrite(it.read, it.write) }, - keyPair = keyPair + signer = signer, + onReady = this::onNewEventCreated ) } - - if (loginWithExternalSigner) { - ExternalSignerUtils.openSigner(event) - val eventContent = ExternalSignerUtils.content[event.id] ?: "" - if (eventContent.isBlank()) { - return - } - event = ContactListEvent.create(event, eventContent) - } - - Client.send(event) - LocalCache.consume(event) } - suspend fun unfollow(user: User) { - if (!isWriteable() && !loginWithExternalSigner) return + fun onNewEventCreated(event: Event) { + Client.send(event) + LocalCache.justConsume(event, null) + } - val contactList = migrateCommunitiesAndChannelsIfNeeded(userProfile().latestContactList) + fun unfollow(user: User) { + if (!isWriteable()) return + + val contactList = userProfile().latestContactList if (contactList != null && contactList.tags.isNotEmpty()) { - var event = ContactListEvent.unfollowUser( + ContactListEvent.unfollowUser( contactList, user.pubkeyHex, - keyPair + signer, + onReady = this::onNewEventCreated ) - - if (loginWithExternalSigner) { - ExternalSignerUtils.openSigner(event) - val eventContent = ExternalSignerUtils.content[event.id] ?: "" - if (eventContent.isBlank()) { - return - } - event = ContactListEvent.create(event, eventContent) - } - - Client.send(event) - LocalCache.consume(event) } } - fun unfollowHashtag(tag: String) { - if (!isWriteable() && !loginWithExternalSigner) return + suspend fun unfollowHashtag(tag: String) { + if (!isWriteable()) return - val contactList = migrateCommunitiesAndChannelsIfNeeded(userProfile().latestContactList) + val contactList = userProfile().latestContactList if (contactList != null && contactList.tags.isNotEmpty()) { - var event = ContactListEvent.unfollowHashtag( + ContactListEvent.unfollowHashtag( contactList, tag, - keyPair + signer, + onReady = this::onNewEventCreated ) - - if (loginWithExternalSigner) { - ExternalSignerUtils.openSigner(event) - val eventContent = ExternalSignerUtils.content[event.id] ?: "" - if (eventContent.isBlank()) { - return - } - event = ContactListEvent.create(event, eventContent) - } - - Client.send(event) - LocalCache.consume(event) } } - fun unfollowGeohash(geohash: String) { - if (!isWriteable() && !loginWithExternalSigner) return + suspend fun unfollowGeohash(geohash: String) { + if (!isWriteable()) return - val contactList = migrateCommunitiesAndChannelsIfNeeded(userProfile().latestContactList) + val contactList = userProfile().latestContactList if (contactList != null && contactList.tags.isNotEmpty()) { - var event = ContactListEvent.unfollowGeohash( + ContactListEvent.unfollowGeohash( contactList, geohash, - keyPair + signer, + onReady = this::onNewEventCreated ) - - if (loginWithExternalSigner) { - ExternalSignerUtils.openSigner(event) - val eventContent = ExternalSignerUtils.content[event.id] ?: "" - if (eventContent.isBlank()) { - return - } - event = ContactListEvent.create(event, eventContent) - } - - Client.send(event) - LocalCache.consume(event) } } - fun unfollow(channel: Channel) { - if (!isWriteable() && !loginWithExternalSigner) return + suspend fun unfollow(channel: Channel) { + if (!isWriteable()) return - val contactList = migrateCommunitiesAndChannelsIfNeeded(userProfile().latestContactList) + val contactList = userProfile().latestContactList if (contactList != null && contactList.tags.isNotEmpty()) { - var event = ContactListEvent.unfollowEvent( + ContactListEvent.unfollowEvent( contactList, channel.idHex, - keyPair + signer, + onReady = this::onNewEventCreated ) - - if (loginWithExternalSigner) { - ExternalSignerUtils.openSigner(event) - val eventContent = ExternalSignerUtils.content[event.id] ?: "" - if (eventContent.isBlank()) { - return - } - event = ContactListEvent.create(event, eventContent) - } - - Client.send(event) - LocalCache.consume(event) } } - fun unfollow(community: AddressableNote) { - if (!isWriteable() && !loginWithExternalSigner) return + suspend fun unfollow(community: AddressableNote) { + if (!isWriteable()) return - val contactList = migrateCommunitiesAndChannelsIfNeeded(userProfile().latestContactList) + val contactList = userProfile().latestContactList if (contactList != null && contactList.tags.isNotEmpty()) { - var event = ContactListEvent.unfollowAddressableEvent( + ContactListEvent.unfollowAddressableEvent( contactList, community.address, - keyPair + signer, + onReady = this::onNewEventCreated ) - - if (loginWithExternalSigner) { - ExternalSignerUtils.openSigner(event) - val eventContent = ExternalSignerUtils.content[event.id] ?: "" - if (eventContent.isBlank()) { - return - } - event = ContactListEvent.create(event, eventContent) - } - - Client.send(event) - LocalCache.consume(event) } } - fun createNip95(byteArray: ByteArray, headerInfo: FileHeader): Pair? { - if (!isWriteable() && !loginWithExternalSigner) return null + fun createNip95(byteArray: ByteArray, headerInfo: FileHeader, onReady: (Pair) -> Unit) { + if (!isWriteable()) return - if (loginWithExternalSigner) { - val unsignedData = FileStorageEvent.create( - mimeType = headerInfo.mimeType ?: "", - data = byteArray, - pubKey = keyPair.pubKey.toHexKey() - ) - - ExternalSignerUtils.openSigner(unsignedData) - val eventContent = ExternalSignerUtils.content[unsignedData.id] ?: "" - if (eventContent.isBlank()) return null - val data = FileStorageEvent( - unsignedData.id, - unsignedData.pubKey, - unsignedData.createdAt, - unsignedData.tags, - unsignedData.content, - eventContent - ) - - val unsignedEvent = FileStorageHeaderEvent.create( + FileStorageEvent.create( + mimeType = headerInfo.mimeType ?: "", + data = byteArray, + signer = signer + ) { data -> + FileStorageHeaderEvent.create( data, mimeType = headerInfo.mimeType, hash = headerInfo.hash, @@ -1225,47 +931,17 @@ class Account( blurhash = headerInfo.blurHash, alt = headerInfo.alt, sensitiveContent = headerInfo.sensitiveContent, - pubKey = keyPair.pubKey.toHexKey() - ) - - ExternalSignerUtils.openSigner(unsignedEvent) - val unsignedEventContent = ExternalSignerUtils.content[unsignedEvent.id] ?: "" - if (unsignedEventContent.isBlank()) return null - val signedEvent = FileStorageHeaderEvent( - unsignedEvent.id, - unsignedEvent.pubKey, - unsignedEvent.createdAt, - unsignedEvent.tags, - unsignedEvent.content, - unsignedEventContent - ) - - return Pair(data, signedEvent) - } else { - val data = FileStorageEvent.create( - mimeType = headerInfo.mimeType ?: "", - data = byteArray, - privateKey = keyPair.privKey!! - ) - - val signedEvent = FileStorageHeaderEvent.create( - data, - mimeType = headerInfo.mimeType, - hash = headerInfo.hash, - size = headerInfo.size.toString(), - dimensions = headerInfo.dim, - blurhash = headerInfo.blurHash, - alt = headerInfo.alt, - sensitiveContent = headerInfo.sensitiveContent, - privateKey = keyPair.privKey!! - ) - - return Pair(data, signedEvent) + signer = signer + ) { signedEvent -> + onReady( + Pair(data, signedEvent) + ) + } } } fun sendNip95(data: FileStorageEvent, signedEvent: FileStorageHeaderEvent, relayList: List? = null): Note? { - if (!isWriteable() && !loginWithExternalSigner) return null + if (!isWriteable()) return null Client.send(data, relayList = relayList) LocalCache.consume(data, null) @@ -1276,55 +952,30 @@ class Account( return LocalCache.notes[signedEvent.id] } - private fun sendHeader(signedEvent: FileHeaderEvent, relayList: List? = null): Note? { + private fun sendHeader(signedEvent: FileHeaderEvent, relayList: List? = null, onReady: (Note) -> Unit) { Client.send(signedEvent, relayList = relayList) LocalCache.consume(signedEvent, null) - return LocalCache.notes[signedEvent.id] + LocalCache.notes[signedEvent.id]?.let { + onReady(it) + } } - fun sendHeader(headerInfo: FileHeader, relayList: List? = null): Note? { - if (!isWriteable() && !loginWithExternalSigner) return null + fun sendHeader(headerInfo: FileHeader, relayList: List? = null, onReady: (Note) -> Unit) { + if (!isWriteable()) return - if (loginWithExternalSigner) { - val unsignedEvent = FileHeaderEvent.create( - url = headerInfo.url, - mimeType = headerInfo.mimeType, - hash = headerInfo.hash, - size = headerInfo.size.toString(), - dimensions = headerInfo.dim, - blurhash = headerInfo.blurHash, - alt = headerInfo.alt, - sensitiveContent = headerInfo.sensitiveContent, - keyPair = keyPair - ) - ExternalSignerUtils.openSigner(unsignedEvent) - val eventContent = ExternalSignerUtils.content[unsignedEvent.id] ?: "" - if (eventContent.isBlank()) return null - val signedEvent = FileHeaderEvent( - unsignedEvent.id, - unsignedEvent.pubKey, - unsignedEvent.createdAt, - unsignedEvent.tags, - unsignedEvent.content, - eventContent - ) - - return sendHeader(signedEvent, relayList = relayList) - } else { - val signedEvent = FileHeaderEvent.create( - url = headerInfo.url, - mimeType = headerInfo.mimeType, - hash = headerInfo.hash, - size = headerInfo.size.toString(), - dimensions = headerInfo.dim, - blurhash = headerInfo.blurHash, - alt = headerInfo.alt, - sensitiveContent = headerInfo.sensitiveContent, - keyPair = keyPair - ) - - return sendHeader(signedEvent, relayList = relayList) + FileHeaderEvent.create( + url = headerInfo.url, + mimeType = headerInfo.mimeType, + hash = headerInfo.hash, + size = headerInfo.size.toString(), + dimensions = headerInfo.dim, + blurhash = headerInfo.blurHash, + alt = headerInfo.alt, + sensitiveContent = headerInfo.sensitiveContent, + signer = signer + ) { event -> + sendHeader(event, relayList = relayList, onReady) } } @@ -1342,13 +993,13 @@ class Account( relayList: List? = null, geohash: String? = null ) { - if (!isWriteable() && !loginWithExternalSigner) return + 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() } - var signedEvent = TextNoteEvent.create( + TextNoteEvent.create( msg = message, replyTos = repliesToHex, mentions = mentionsHex, @@ -1361,35 +1012,26 @@ class Account( root = root, directMentions = directMentions, geohash = geohash, - keyPair = keyPair - ) + signer = signer + ) { + Client.send(it, relayList = relayList) + LocalCache.justConsume(it, null) - if (loginWithExternalSigner) { - ExternalSignerUtils.openSigner(signedEvent) - val eventContent = ExternalSignerUtils.content[signedEvent.id] ?: "" - if (eventContent.isBlank()) { - return + // broadcast replied notes + replyingTo?.let { + LocalCache.getNoteIfExists(replyingTo)?.event?.let { + Client.send(it, relayList = relayList) + } } - signedEvent = TextNoteEvent.create(signedEvent, eventContent) - } - - Client.send(signedEvent, relayList = relayList) - LocalCache.consume(signedEvent) - - // broadcast replied notes - replyingTo?.let { - LocalCache.getNoteIfExists(replyingTo)?.event?.let { - Client.send(it, relayList = relayList) + replyTo?.forEach { + it.event?.let { + Client.send(it, relayList = relayList) + } } - } - replyTo?.forEach { - it.event?.let { - Client.send(it, relayList = relayList) - } - } - addresses?.forEach { - LocalCache.getAddressableNoteIfExists(it.toTag())?.event?.let { - Client.send(it, relayList = relayList) + addresses?.forEach { + LocalCache.getAddressableNoteIfExists(it.toTag())?.event?.let { + Client.send(it, relayList = relayList) + } } } } @@ -1409,19 +1051,18 @@ class Account( relayList: List? = null, geohash: String? = null ) { - if (!isWriteable() && !loginWithExternalSigner) return + if (!isWriteable()) return val repliesToHex = replyTo?.map { it.idHex } val mentionsHex = mentions?.map { it.pubkeyHex } val addresses = replyTo?.mapNotNull { it.address() } - var signedEvent = PollNoteEvent.create( + PollNoteEvent.create( msg = message, replyTos = repliesToHex, mentions = mentionsHex, addresses = addresses, - pubKey = keyPair.pubKey.toHexKey(), - privateKey = keyPair.privKey, + signer = signer, pollOptions = pollOptions, valueMaximum = valueMaximum, valueMinimum = valueMinimum, @@ -1431,41 +1072,31 @@ class Account( markAsSensitive = wantsToMarkAsSensitive, zapRaiserAmount = zapRaiserAmount, geohash = geohash - ) - // println("Sending new PollNoteEvent: %s".format(signedEvent.toJson())) + ) { + Client.send(it, relayList = relayList) + LocalCache.justConsume(it, null) - if (loginWithExternalSigner) { - ExternalSignerUtils.openSigner(signedEvent) - val eventContent = ExternalSignerUtils.content[signedEvent.id] ?: "" - if (eventContent.isBlank()) { - return + // Rebroadcast replies and tags to the current relay set + replyTo?.forEach { + it.event?.let { + Client.send(it, relayList = relayList) + } } - signedEvent = PollNoteEvent.create(signedEvent, eventContent) - } - - Client.send(signedEvent, relayList = relayList) - LocalCache.consume(signedEvent) - - replyTo?.forEach { - it.event?.let { - Client.send(it, relayList = relayList) - } - } - addresses?.forEach { - LocalCache.getAddressableNoteIfExists(it.toTag())?.event?.let { - Client.send(it, relayList = relayList) + addresses?.forEach { + LocalCache.getAddressableNoteIfExists(it.toTag())?.event?.let { + Client.send(it, relayList = relayList) + } } } } fun sendChannelMessage(message: String, toChannel: String, replyTo: List?, mentions: List?, zapReceiver: List? = null, wantsToMarkAsSensitive: Boolean, zapRaiserAmount: Long? = null, geohash: String? = null) { - if (!isWriteable() && !loginWithExternalSigner) return + if (!isWriteable()) return - // val repliesToHex = listOfNotNull(replyingTo?.idHex).ifEmpty { null } val repliesToHex = replyTo?.map { it.idHex } val mentionsHex = mentions?.map { it.pubkeyHex } - var signedEvent = ChannelMessageEvent.create( + ChannelMessageEvent.create( message = message, channel = toChannel, replyTos = repliesToHex, @@ -1474,30 +1105,21 @@ class Account( markAsSensitive = wantsToMarkAsSensitive, zapRaiserAmount = zapRaiserAmount, geohash = geohash, - keyPair = keyPair - ) - - if (loginWithExternalSigner) { - ExternalSignerUtils.openSigner(signedEvent) - val eventContent = ExternalSignerUtils.content[signedEvent.id] ?: "" - if (eventContent.isBlank()) { - return - } - signedEvent = ChannelMessageEvent.create(signedEvent, eventContent) + signer = signer + ) { + Client.send(it) + LocalCache.justConsume(it, null) } - - Client.send(signedEvent) - LocalCache.consume(signedEvent, null) } fun sendLiveMessage(message: String, toChannel: ATag, replyTo: List?, mentions: List?, zapReceiver: List? = null, wantsToMarkAsSensitive: Boolean, zapRaiserAmount: Long? = null, geohash: String? = null) { - if (!isWriteable() && !loginWithExternalSigner) return + if (!isWriteable()) return // val repliesToHex = listOfNotNull(replyingTo?.idHex).ifEmpty { null } val repliesToHex = replyTo?.map { it.idHex } val mentionsHex = mentions?.map { it.pubkeyHex } - var signedEvent = LiveActivitiesChatMessageEvent.create( + LiveActivitiesChatMessageEvent.create( message = message, activity = toChannel, replyTos = repliesToHex, @@ -1506,20 +1128,11 @@ class Account( markAsSensitive = wantsToMarkAsSensitive, zapRaiserAmount = zapRaiserAmount, geohash = geohash, - keyPair = keyPair - ) - - if (loginWithExternalSigner) { - ExternalSignerUtils.openSigner(signedEvent) - val eventContent = ExternalSignerUtils.content[signedEvent.id] ?: "" - if (eventContent.isBlank()) { - return - } - signedEvent = LiveActivitiesChatMessageEvent.create(signedEvent, eventContent) + signer = signer + ) { + Client.send(it) + LocalCache.justConsume(it, null) } - - Client.send(signedEvent) - LocalCache.consume(signedEvent, null) } fun sendPrivateMessage(message: String, toUser: User, replyingTo: Note? = null, mentions: List?, zapReceiver: List? = null, wantsToMarkAsSensitive: Boolean, zapRaiserAmount: Long? = null, geohash: String? = null) { @@ -1527,55 +1140,26 @@ class Account( } fun sendPrivateMessage(message: String, toUser: HexKey, replyingTo: Note? = null, mentions: List?, zapReceiver: List? = null, wantsToMarkAsSensitive: Boolean, zapRaiserAmount: Long? = null, geohash: String? = null) { - if (!isWriteable() && !loginWithExternalSigner) return + if (!isWriteable()) return val repliesToHex = listOfNotNull(replyingTo?.idHex).ifEmpty { null } val mentionsHex = mentions?.map { it.pubkeyHex } - if (loginWithExternalSigner) { - ExternalSignerUtils.encrypt(message, toUser, "encrypt") - val eventContent = ExternalSignerUtils.content["encrypt"] ?: "" - if (eventContent.isBlank()) return - ExternalSignerUtils.content.remove("encrypt") - val unsignedEvent = PrivateDmEvent.create( - recipientPubKey = toUser.hexToByteArray(), - publishedRecipientPubKey = toUser.hexToByteArray(), - msg = eventContent, - replyTos = repliesToHex, - mentions = mentionsHex, - zapReceiver = zapReceiver, - markAsSensitive = wantsToMarkAsSensitive, - zapRaiserAmount = zapRaiserAmount, - geohash = geohash, - keyPair = keyPair, - advertiseNip18 = false - ) - - ExternalSignerUtils.openSigner(unsignedEvent) - val signature = ExternalSignerUtils.content[unsignedEvent.id] ?: "" - if (signature.isBlank()) { - return - } - val signedEvent = PrivateDmEvent.create(unsignedEvent, signature) - Client.send(signedEvent) - LocalCache.consume(signedEvent, null) - } else { - val signedEvent = PrivateDmEvent.create( - recipientPubKey = toUser.hexToByteArray(), - publishedRecipientPubKey = toUser.hexToByteArray(), - msg = message, - replyTos = repliesToHex, - mentions = mentionsHex, - zapReceiver = zapReceiver, - markAsSensitive = wantsToMarkAsSensitive, - zapRaiserAmount = zapRaiserAmount, - geohash = geohash, - keyPair = keyPair, - advertiseNip18 = false - ) - - Client.send(signedEvent) - LocalCache.consume(signedEvent, null) + PrivateDmEvent.create( + recipientPubKey = toUser, + publishedRecipientPubKey = toUser, + msg = message, + replyTos = repliesToHex, + mentions = mentionsHex, + zapReceiver = zapReceiver, + markAsSensitive = wantsToMarkAsSensitive, + zapRaiserAmount = zapRaiserAmount, + geohash = geohash, + signer = signer, + advertiseNip18 = false + ) { + Client.send(it) + LocalCache.consume(it, null) } } @@ -1590,106 +1174,44 @@ class Account( zapRaiserAmount: Long? = null, geohash: String? = null ) { - if (!isWriteable() && !loginWithExternalSigner) return + if (!isWriteable()) return val repliesToHex = listOfNotNull(replyingTo?.idHex).ifEmpty { null } val mentionsHex = mentions?.map { it.pubkeyHex } - if (loginWithExternalSigner) { - var chatMessageEvent = ChatMessageEvent.create( - msg = message, - to = toUsers, - keyPair = keyPair, - subject = subject, - replyTos = repliesToHex, - mentions = mentionsHex, - zapReceiver = zapReceiver, - markAsSensitive = wantsToMarkAsSensitive, - zapRaiserAmount = zapRaiserAmount, - geohash = geohash - ) - - ExternalSignerUtils.openSigner(chatMessageEvent) - val eventContent = ExternalSignerUtils.content[chatMessageEvent.id] ?: "" - if (eventContent.isBlank()) return - chatMessageEvent = ChatMessageEvent.create(chatMessageEvent, eventContent) - val senderPublicKey = keyPair.pubKey.toHexKey() - toUsers.plus(senderPublicKey).toSet().forEach { - val gossip = Gossip.create(chatMessageEvent) - val content = Gossip.toJson(gossip) - ExternalSignerUtils.encrypt(content, it, gossip.id!!, SignerType.NIP44_ENCRYPT) - val gossipContent = ExternalSignerUtils.content[gossip.id] ?: "" - if (gossipContent.isNotBlank()) { - var sealedEvent = SealedGossipEvent.create( - encryptedContent = gossipContent, - pubKey = senderPublicKey - ) - ExternalSignerUtils.openSigner(sealedEvent) - val sealedEventContent = ExternalSignerUtils.content[sealedEvent.id] ?: "" - if (sealedEventContent.isBlank()) return - sealedEvent = SealedGossipEvent.create(sealedEvent, sealedEventContent) - - val giftWraps = GiftWrapEvent.create( - event = sealedEvent, - recipientPubKey = it - ) - broadcastPrivately(NIP24Factory.Result(chatMessageEvent, listOf(giftWraps))) - } - } - } else { - val signedEvents = NIP24Factory().createMsgNIP24( - msg = message, - to = toUsers, - subject = subject, - replyTos = repliesToHex, - mentions = mentionsHex, - zapReceiver = zapReceiver, - markAsSensitive = wantsToMarkAsSensitive, - zapRaiserAmount = zapRaiserAmount, - geohash = geohash, - keyPair = keyPair - ) - - broadcastPrivately(signedEvents) + NIP24Factory().createMsgNIP24( + msg = message, + to = toUsers, + subject = subject, + replyTos = repliesToHex, + mentions = mentionsHex, + zapReceiver = zapReceiver, + markAsSensitive = wantsToMarkAsSensitive, + zapRaiserAmount = zapRaiserAmount, + geohash = geohash, + signer = signer + ) { + broadcastPrivately(it) } } fun broadcastPrivately(signedEvents: NIP24Factory.Result) { val mine = signedEvents.wraps.filter { - (it.recipientPubKey() == keyPair.pubKey.toHexKey()) + (it.recipientPubKey() == signer.pubKey) } - mine.forEach { - // Only keep in cache the GiftWrap for the account. - if (loginWithExternalSigner) { - ExternalSignerUtils.decrypt(it.content, it.pubKey, it.id, SignerType.NIP44_DECRYPT) - val decryptedContent = ExternalSignerUtils.cachedDecryptedContent[it.id] ?: "" - if (decryptedContent.isEmpty()) return - it.cachedGift(keyPair.pubKey, decryptedContent)?.let { cached -> - if (cached is SealedGossipEvent) { - ExternalSignerUtils.decrypt(cached.content, cached.pubKey, cached.id, SignerType.NIP44_DECRYPT) - val localDecryptedContent = ExternalSignerUtils.cachedDecryptedContent[cached.id] ?: "" - if (localDecryptedContent.isEmpty()) return - cached.cachedGossip(keyPair.pubKey, localDecryptedContent)?.let { gossip -> - LocalCache.justConsume(gossip, null) - } - } else { - LocalCache.justConsume(it, null) - } - } - } else { - it.cachedGift(keyPair.privKey!!)?.let { - if (it is SealedGossipEvent) { - it.cachedGossip(keyPair.privKey!!)?.let { - LocalCache.justConsume(it, null) - } - } else { - LocalCache.justConsume(it, null) + mine.forEach { giftWrap -> + giftWrap.cachedGift(signer) { gift -> + if (gift is SealedGossipEvent) { + gift.cachedGossip(signer) { gossip -> + LocalCache.justConsume(gossip, null) } + } else { + LocalCache.justConsume(gift, null) } } - LocalCache.consume(it, null) + LocalCache.consume(giftWrap, null) } val id = mine.firstOrNull()?.id @@ -1706,134 +1228,87 @@ class Account( } fun sendCreateNewChannel(name: String, about: String, picture: String) { - if (!isWriteable() && !loginWithExternalSigner) return + if (!isWriteable()) return - val metadata = ChannelCreateEvent.ChannelData( - name, - about, - picture - ) + ChannelCreateEvent.create( + name = name, + about = about, + picture = picture, + signer = signer + ) { + Client.send(it) + LocalCache.justConsume(it, null) - var event = ChannelCreateEvent.create( - channelInfo = metadata, - keyPair = keyPair - ) - - if (loginWithExternalSigner) { - ExternalSignerUtils.openSigner(event) - val eventContent = ExternalSignerUtils.content[event.id] ?: "" - if (eventContent.isBlank()) { - return + LocalCache.getChannelIfExists(it.id)?.let { + follow(it) } - event = ChannelCreateEvent.create(event, eventContent) - } - - Client.send(event) - LocalCache.consume(event) - - LocalCache.getChannelIfExists(event.id)?.let { - follow(it) } } fun updateStatus(oldStatus: AddressableNote, newStatus: String) { - if (!isWriteable() && !loginWithExternalSigner) return + if (!isWriteable()) return val oldEvent = oldStatus.event as? StatusEvent ?: return - var event = StatusEvent.update(oldEvent, newStatus, keyPair) - if (loginWithExternalSigner) { - ExternalSignerUtils.openSigner(event) - val eventContent = ExternalSignerUtils.content[event.id] ?: "" - if (eventContent.isBlank()) { - return - } - event = StatusEvent.create(event, eventContent) + StatusEvent.update(oldEvent, newStatus, signer) { + Client.send(it) + LocalCache.justConsume(it, null) } - Client.send(event) - LocalCache.consume(event, null) } fun createStatus(newStatus: String) { - if (!isWriteable() && !loginWithExternalSigner) return + if (!isWriteable()) return - var event = StatusEvent.create(newStatus, "general", expiration = null, keyPair) - if (loginWithExternalSigner) { - ExternalSignerUtils.openSigner(event) - val eventContent = ExternalSignerUtils.content[event.id] ?: "" - if (eventContent.isBlank()) { - return - } - event = StatusEvent.create(event, eventContent) + StatusEvent.create(newStatus, "general", expiration = null, signer) { + Client.send(it) + LocalCache.justConsume(it, null) } - Client.send(event) - LocalCache.consume(event, null) } fun deleteStatus(oldStatus: AddressableNote) { - if (!isWriteable() && !loginWithExternalSigner) return + if (!isWriteable()) return val oldEvent = oldStatus.event as? StatusEvent ?: return - var event = StatusEvent.clear(oldEvent, keyPair) - if (loginWithExternalSigner) { - ExternalSignerUtils.openSigner(event) - val eventContent = ExternalSignerUtils.content[event.id] ?: "" - if (eventContent.isBlank()) { - return - } - event = StatusEvent.create(event, eventContent) - } - Client.send(event) - LocalCache.consume(event, null) + StatusEvent.clear(oldEvent, signer) { event -> + Client.send(event) + LocalCache.justConsume(event, null) - var event2 = DeletionEvent.create(listOf(event.id), keyPair) - if (loginWithExternalSigner) { - ExternalSignerUtils.openSigner(event2) - val event2Content = ExternalSignerUtils.content[event2.id] ?: "" - if (event2Content.isBlank()) { - return + DeletionEvent.create(listOf(event.id), signer) { event2 -> + Client.send(event2) + LocalCache.justConsume(event2, null) } - event2 = DeletionEvent.create(event2, event2Content) } - Client.send(event2) - LocalCache.consume(event2) } fun removeEmojiPack(usersEmojiList: Note, emojiList: Note) { - if (!isWriteable() && !loginWithExternalSigner) return + if (!isWriteable()) return val noteEvent = usersEmojiList.event if (noteEvent !is EmojiPackSelectionEvent) return val emojiListEvent = emojiList.event if (emojiListEvent !is EmojiPackEvent) return - var event = EmojiPackSelectionEvent.create( + EmojiPackSelectionEvent.create( noteEvent.taggedAddresses().filter { it != emojiListEvent.address() }, - keyPair - ) - - if (loginWithExternalSigner) { - ExternalSignerUtils.openSigner(event) - val eventContent = ExternalSignerUtils.content[event.id] ?: "" - if (eventContent.isBlank()) { - return - } - event = EmojiPackSelectionEvent.create(event, eventContent) + signer + ) { + Client.send(it) + LocalCache.justConsume(it, null) } - - Client.send(event) - LocalCache.consume(event) } fun addEmojiPack(usersEmojiList: Note, emojiList: Note) { - if (!isWriteable() && !loginWithExternalSigner) return + if (!isWriteable()) return val emojiListEvent = emojiList.event if (emojiListEvent !is EmojiPackEvent) return - var event = if (usersEmojiList.event == null) { + if (usersEmojiList.event == null) { EmojiPackSelectionEvent.create( listOf(emojiListEvent.address()), - keyPair - ) + signer + ) { + Client.send(it) + LocalCache.justConsume(it, null) + } } else { val noteEvent = usersEmojiList.event if (noteEvent !is EmojiPackSelectionEvent) return @@ -1844,442 +1319,94 @@ class Account( EmojiPackSelectionEvent.create( noteEvent.taggedAddresses().plus(emojiListEvent.address()), - keyPair - ) - } - - if (loginWithExternalSigner) { - ExternalSignerUtils.openSigner(event) - val eventContent = ExternalSignerUtils.content[event.id] ?: "" - if (eventContent.isBlank()) { - return + signer + ) { + Client.send(it) + LocalCache.justConsume(it, null) } - event = EmojiPackSelectionEvent.create(event, eventContent) } - - Client.send(event) - LocalCache.consume(event) } - fun addPrivateBookmark(note: Note, decryptedContent: String) { - val bookmarks = userProfile().latestBookmarkList - val privTags = mutableListOf>() - - val privEvents = if (note is AddressableNote) { - bookmarks?.privateTaggedEvents(decryptedContent) ?: emptyList() - } else { - bookmarks?.privateTaggedEvents(decryptedContent)?.plus(note.idHex) ?: listOf(note.idHex) - } - val privUsers = bookmarks?.privateTaggedUsers(decryptedContent) ?: emptyList() - val privAddresses = if (note is AddressableNote) { - bookmarks?.privateTaggedAddresses(decryptedContent)?.plus(note.address) ?: listOf(note.address) - } else { - bookmarks?.privateTaggedAddresses(decryptedContent) ?: emptyList() - } - - privEvents.forEach { - privTags.add(listOf("e", it)) - } - privUsers.forEach { - privTags.add(listOf("p", it)) - } - privAddresses.forEach { - privTags.add(listOf("a", it.toTag())) - } - val msg = Event.mapper.writeValueAsString(privTags) - - ExternalSignerUtils.encrypt(msg, keyPair.pubKey.toHexKey(), "encrypt") - val encryptedContent = ExternalSignerUtils.content["encrypt"] ?: "" - ExternalSignerUtils.content.remove("encrypt") - if (encryptedContent.isBlank()) { - return - } - - var event = BookmarkListEvent.create( - "bookmark", - bookmarks?.taggedEvents() ?: emptyList(), - bookmarks?.taggedUsers() ?: emptyList(), - bookmarks?.taggedAddresses() ?: emptyList(), - - encryptedContent, - - keyPair.pubKey.toHexKey() - ) - - ExternalSignerUtils.openSigner(event) - val eventContent = ExternalSignerUtils.content[event.id] ?: "" - if (eventContent.isBlank()) { - return - } - event = BookmarkListEvent.create(event, eventContent) - - Client.send(event) - LocalCache.consume(event) - } - - fun removePrivateBookmark(note: Note, decryptedContent: String) { - val bookmarks = userProfile().latestBookmarkList - val privTags = mutableListOf>() - - val privEvents = if (note is AddressableNote) { - bookmarks?.privateTaggedEvents(decryptedContent) ?: emptyList() - } else { - bookmarks?.privateTaggedEvents(decryptedContent)?.minus(note.idHex) ?: listOf(note.idHex) - } - val privUsers = bookmarks?.privateTaggedUsers(decryptedContent) ?: emptyList() - val privAddresses = if (note is AddressableNote) { - bookmarks?.privateTaggedAddresses(decryptedContent)?.minus(note.address) ?: listOf(note.address) - } else { - bookmarks?.privateTaggedAddresses(decryptedContent) ?: emptyList() - } - - privEvents.forEach { - privTags.add(listOf("e", it)) - } - privUsers.forEach { - privTags.add(listOf("p", it)) - } - privAddresses.forEach { - privTags.add(listOf("a", it.toTag())) - } - val msg = Event.mapper.writeValueAsString(privTags) - - ExternalSignerUtils.encrypt(msg, keyPair.pubKey.toHexKey(), "encrypt") - val encryptedContent = ExternalSignerUtils.content["encrypt"] ?: "" - ExternalSignerUtils.content.remove("encrypt") - if (encryptedContent.isBlank()) { - return - } - - var event = BookmarkListEvent.create( - "bookmark", - bookmarks?.taggedEvents() ?: emptyList(), - bookmarks?.taggedUsers() ?: emptyList(), - bookmarks?.taggedAddresses() ?: emptyList(), - - encryptedContent, - - keyPair.pubKey.toHexKey() - ) - - ExternalSignerUtils.openSigner(event) - val eventContent = ExternalSignerUtils.content[event.id] ?: "" - if (eventContent.isBlank()) { - return - } - event = BookmarkListEvent.create(event, eventContent) - - Client.send(event) - LocalCache.consume(event) - } - - fun addPrivateBookmark(note: Note) { + fun addBookmark(note: Note, isPrivate: Boolean) { if (!isWriteable()) return - val bookmarks = userProfile().latestBookmarkList - - val event = if (note is AddressableNote) { - BookmarkListEvent.create( - "bookmark", - bookmarks?.taggedEvents() ?: emptyList(), - bookmarks?.taggedUsers() ?: emptyList(), - bookmarks?.taggedAddresses() ?: emptyList(), - - bookmarks?.privateTaggedEvents(privKey = keyPair.privKey!!) ?: emptyList(), - bookmarks?.privateTaggedUsers(privKey = keyPair.privKey!!) ?: emptyList(), - bookmarks?.privateTaggedAddresses(privKey = keyPair.privKey!!)?.plus(note.address) ?: listOf(note.address), - - keyPair.privKey!! - ) - } else { - BookmarkListEvent.create( - "bookmark", - bookmarks?.taggedEvents() ?: emptyList(), - bookmarks?.taggedUsers() ?: emptyList(), - bookmarks?.taggedAddresses() ?: emptyList(), - - bookmarks?.privateTaggedEvents(privKey = keyPair.privKey!!)?.plus(note.idHex) ?: listOf(note.idHex), - bookmarks?.privateTaggedUsers(privKey = keyPair.privKey!!) ?: emptyList(), - bookmarks?.privateTaggedAddresses(privKey = keyPair.privKey!!) ?: emptyList(), - - keyPair.privKey!! - ) - } - - Client.send(event) - LocalCache.consume(event) - } - - fun addPublicBookmark(note: Note, decryptedContent: String) { - val bookmarks = userProfile().latestBookmarkList - - val privTags = mutableListOf>() - - val privEvents = bookmarks?.privateTaggedEvents(decryptedContent) ?: emptyList() - val privUsers = bookmarks?.privateTaggedUsers(decryptedContent) ?: emptyList() - val privAddresses = bookmarks?.privateTaggedAddresses(decryptedContent) ?: emptyList() - - privEvents.forEach { - privTags.add(listOf("e", it)) - } - privUsers.forEach { - privTags.add(listOf("p", it)) - } - privAddresses.forEach { - privTags.add(listOf("a", it.toTag())) - } - val msg = Event.mapper.writeValueAsString(privTags) - - ExternalSignerUtils.encrypt(msg, keyPair.pubKey.toHexKey(), "encrypt") - val encryptedContent = ExternalSignerUtils.content["encrypt"] ?: "" - ExternalSignerUtils.content.remove("encrypt") - if (encryptedContent.isBlank()) { - return - } - - var event = if (note is AddressableNote) { - BookmarkListEvent.create( - "bookmark", - bookmarks?.taggedEvents() ?: emptyList(), - bookmarks?.taggedUsers() ?: emptyList(), - bookmarks?.taggedAddresses()?.plus(note.address) ?: listOf(note.address), - encryptedContent, - keyPair.pubKey.toHexKey() - ) - } else { - BookmarkListEvent.create( - "bookmark", - bookmarks?.taggedEvents()?.plus(note.idHex) ?: listOf(note.idHex), - bookmarks?.taggedUsers() ?: emptyList(), - bookmarks?.taggedAddresses() ?: emptyList(), - encryptedContent, - keyPair.pubKey.toHexKey() - ) - } - ExternalSignerUtils.openSigner(event) - val eventContent = ExternalSignerUtils.content[event.id] ?: "" - if (eventContent.isBlank()) { - return - } - event = BookmarkListEvent.create(event, encryptedContent) - - Client.send(event) - LocalCache.consume(event) - } - - fun addPublicBookmark(note: Note) { - if (!isWriteable()) return - - val bookmarks = userProfile().latestBookmarkList - - val event = if (note is AddressableNote) { - BookmarkListEvent.create( - "bookmark", - bookmarks?.taggedEvents() ?: emptyList(), - bookmarks?.taggedUsers() ?: emptyList(), - bookmarks?.taggedAddresses()?.plus(note.address) ?: listOf(note.address), - - bookmarks?.privateTaggedEvents(privKey = keyPair.privKey!!) ?: emptyList(), - bookmarks?.privateTaggedUsers(privKey = keyPair.privKey!!) ?: emptyList(), - bookmarks?.privateTaggedAddresses(privKey = keyPair.privKey!!) ?: emptyList(), - - keyPair.privKey!! - ) - } else { - BookmarkListEvent.create( - "bookmark", - bookmarks?.taggedEvents()?.plus(note.idHex) ?: listOf(note.idHex), - bookmarks?.taggedUsers() ?: emptyList(), - bookmarks?.taggedAddresses() ?: emptyList(), - - bookmarks?.privateTaggedEvents(privKey = keyPair.privKey!!) ?: emptyList(), - bookmarks?.privateTaggedUsers(privKey = keyPair.privKey!!) ?: emptyList(), - bookmarks?.privateTaggedAddresses(privKey = keyPair.privKey!!) ?: emptyList(), - - keyPair.privKey!! - ) - } - - Client.send(event) - LocalCache.consume(event) - } - - fun removePrivateBookmark(note: Note) { - if (!isWriteable()) return - - val bookmarks = userProfile().latestBookmarkList - - val event = if (note is AddressableNote) { - BookmarkListEvent.create( - "bookmark", - bookmarks?.taggedEvents() ?: emptyList(), - bookmarks?.taggedUsers() ?: emptyList(), - bookmarks?.taggedAddresses() ?: emptyList(), - - bookmarks?.privateTaggedEvents(privKey = keyPair.privKey!!) ?: emptyList(), - bookmarks?.privateTaggedUsers(privKey = keyPair.privKey!!) ?: emptyList(), - bookmarks?.privateTaggedAddresses(privKey = keyPair.privKey!!)?.minus(note.address) ?: listOf(), - - keyPair.privKey!! - ) - } else { - BookmarkListEvent.create( - "bookmark", - bookmarks?.taggedEvents() ?: emptyList(), - bookmarks?.taggedUsers() ?: emptyList(), - bookmarks?.taggedAddresses() ?: emptyList(), - - bookmarks?.privateTaggedEvents(privKey = keyPair.privKey!!)?.minus(note.idHex) ?: listOf(), - bookmarks?.privateTaggedUsers(privKey = keyPair.privKey!!) ?: emptyList(), - bookmarks?.privateTaggedAddresses(privKey = keyPair.privKey!!) ?: emptyList(), - - keyPair.privKey!! - ) - } - - Client.send(event) - LocalCache.consume(event) - } - - fun createAuthEvent(relay: Relay, challenge: String): RelayAuthEvent? { - return createAuthEvent(relay.url, challenge) - } - - fun createAuthEvent(relayUrl: String, challenge: String): RelayAuthEvent? { - if (!isWriteable() && !loginWithExternalSigner) return null - - var event = RelayAuthEvent.create(relayUrl, challenge, keyPair.pubKey.toHexKey(), keyPair.privKey) - if (loginWithExternalSigner) { - ExternalSignerUtils.openSigner(event) - val eventContent = ExternalSignerUtils.content[event.id] ?: "" - if (eventContent.isBlank()) { - return null - } - event = RelayAuthEvent.create(event, eventContent) - } - - return event - } - - fun removePublicBookmark(note: Note) { - if (!isWriteable()) return - - val bookmarks = userProfile().latestBookmarkList - - val event = if (note is AddressableNote) { - BookmarkListEvent.create( - "bookmark", - bookmarks?.taggedEvents()?.minus(note.address.toTag()) ?: emptyList(), - bookmarks?.taggedUsers() ?: emptyList(), - bookmarks?.taggedAddresses()?.minus(note.address), - - bookmarks?.privateTaggedEvents(privKey = keyPair.privKey!!) ?: emptyList(), - bookmarks?.privateTaggedUsers(privKey = keyPair.privKey!!) ?: emptyList(), - bookmarks?.privateTaggedAddresses(privKey = keyPair.privKey!!) ?: emptyList(), - - keyPair.privKey!! - ) - } else { - BookmarkListEvent.create( - "bookmark", - bookmarks?.taggedEvents()?.minus(note.idHex), - bookmarks?.taggedUsers() ?: emptyList(), - bookmarks?.taggedAddresses() ?: emptyList(), - - bookmarks?.privateTaggedEvents(privKey = keyPair.privKey!!) ?: emptyList(), - bookmarks?.privateTaggedUsers(privKey = keyPair.privKey!!) ?: emptyList(), - bookmarks?.privateTaggedAddresses(privKey = keyPair.privKey!!) ?: emptyList(), - - keyPair.privKey!! - ) - } - - Client.send(event) - LocalCache.consume(event) - } - - fun removePublicBookmark(note: Note, decryptedContent: String) { - val bookmarks = userProfile().latestBookmarkList - - val privTags = mutableListOf>() - - val privEvents = bookmarks?.privateTaggedEvents(decryptedContent) ?: emptyList() - val privUsers = bookmarks?.privateTaggedUsers(decryptedContent) ?: emptyList() - val privAddresses = bookmarks?.privateTaggedAddresses(decryptedContent) ?: emptyList() - - privEvents.forEach { - privTags.add(listOf("e", it)) - } - privUsers.forEach { - privTags.add(listOf("p", it)) - } - privAddresses.forEach { - privTags.add(listOf("a", it.toTag())) - } - val msg = Event.mapper.writeValueAsString(privTags) - - ExternalSignerUtils.encrypt(msg, keyPair.pubKey.toHexKey(), "encrypt") - val encryptedContent = ExternalSignerUtils.content["encrypt"] ?: "" - ExternalSignerUtils.content.remove("encrypt") - if (encryptedContent.isBlank()) { - return - } - - var event = if (note is AddressableNote) { - BookmarkListEvent.create( - "bookmark", - bookmarks?.taggedEvents() ?: emptyList(), - bookmarks?.taggedUsers() ?: emptyList(), - bookmarks?.taggedAddresses()?.minus(note.address), - encryptedContent, - keyPair.pubKey.toHexKey() - ) - } else { - BookmarkListEvent.create( - "bookmark", - bookmarks?.taggedEvents()?.minus(note.idHex), - bookmarks?.taggedUsers() ?: emptyList(), - bookmarks?.taggedAddresses() ?: emptyList(), - encryptedContent, - keyPair.pubKey.toHexKey() - ) - } - - ExternalSignerUtils.openSigner(event) - val eventContent = ExternalSignerUtils.content[event.id] ?: "" - if (eventContent.isBlank()) { - return - } - event = BookmarkListEvent.create(event, eventContent) - - Client.send(event) - LocalCache.consume(event) - } - - fun isInPrivateBookmarks(note: Note): Boolean { - if (!isWriteable() && !loginWithExternalSigner) return false - - if (loginWithExternalSigner) { - return if (note is AddressableNote) { - userProfile().latestBookmarkList?.privateTaggedAddresses(userProfile().latestBookmarkList?.decryptedContent ?: "") - ?.contains(note.address) == true - } else { - userProfile().latestBookmarkList?.privateTaggedEvents(userProfile().latestBookmarkList?.decryptedContent ?: "") - ?.contains(note.idHex) == true + if (note is AddressableNote) { + BookmarkListEvent.addReplaceable( + userProfile().latestBookmarkList, + note.address, + isPrivate, + signer + ) { + Client.send(it) + LocalCache.consume(it) } } else { - return if (note is AddressableNote) { - userProfile().latestBookmarkList?.privateTaggedAddresses(keyPair.privKey!!) - ?.contains(note.address) == true - } else { - userProfile().latestBookmarkList?.privateTaggedEvents(keyPair.privKey!!) - ?.contains(note.idHex) == true + BookmarkListEvent.addEvent( + userProfile().latestBookmarkList, + note.idHex, + isPrivate, + signer + ) { + Client.send(it) + LocalCache.consume(it) + } + } + } + + fun removeBookmark(note: Note, isPrivate: Boolean) { + if (!isWriteable()) return + + val bookmarks = userProfile().latestBookmarkList ?: return + + if (note is AddressableNote) { + BookmarkListEvent.removeReplaceable( + bookmarks, + note.address, + isPrivate, + signer + ) { + Client.send(it) + LocalCache.consume(it) + } + } else { + BookmarkListEvent.removeEvent( + bookmarks, + note.idHex, + isPrivate, + signer + ) { + Client.send(it) + LocalCache.consume(it) + } + } + } + + fun createAuthEvent(relay: Relay, challenge: String, onReady: (RelayAuthEvent) -> Unit) { + return createAuthEvent(relay.url, challenge, onReady = onReady) + } + + fun createAuthEvent(relayUrl: String, challenge: String, onReady: (RelayAuthEvent) -> Unit) { + if (!isWriteable()) return + + RelayAuthEvent.create(relayUrl, challenge, signer, onReady = onReady) + } + + fun isInPrivateBookmarks(note: Note, onReady: (Boolean) -> Unit) { + if (!isWriteable()) return + + if (note is AddressableNote) { + userProfile().latestBookmarkList?.privateTaggedAddresses(signer) { + onReady(it.contains(note.address)) + } + } else { + userProfile().latestBookmarkList?.privateTaggedEvents(signer) { + onReady(it.contains(note.idHex)) } } } fun isInPublicBookmarks(note: Note): Boolean { - if (!isWriteable() && !loginWithExternalSigner) return false + if (!isWriteable()) return false if (note is AddressableNote) { return userProfile().latestBookmarkList?.taggedAddresses()?.contains(note.address) == true @@ -2302,318 +1429,107 @@ class Account( return getBlockListNote().event as? PeopleListEvent } - private fun migrateHiddenUsersIfNeeded(latestList: PeopleListEvent?): PeopleListEvent? { - if (latestList == null) return latestList - - var returningList: PeopleListEvent = latestList - - if (hiddenUsers.isNotEmpty()) { - returningList = PeopleListEvent.addUsers(returningList, hiddenUsers.toList(), true, keyPair.privKey!!) - hiddenUsers = emptySet() - } - - return returningList - } - fun hideWord(word: String) { - val blockList = migrateHiddenUsersIfNeeded(getBlockList()) - if (loginWithExternalSigner) { - val id = blockList?.id - val encryptedContent = if (id == null) { - val privateTags = listOf(listOf("word", word)) - val msg = Event.mapper.writeValueAsString(privateTags) + val blockList = getBlockList() - ExternalSignerUtils.encrypt(msg, keyPair.pubKey.toHexKey(), "encrypted") - val encryptedContent = ExternalSignerUtils.content["encrypted"] ?: "" - ExternalSignerUtils.content.remove("encrypted") - if (encryptedContent.isBlank()) return - encryptedContent - } else { - var decryptedContent = ExternalSignerUtils.cachedDecryptedContent[id] - if (decryptedContent == null) { - ExternalSignerUtils.decrypt(blockList.content, keyPair.pubKey.toHexKey(), id) - val content = ExternalSignerUtils.content[id] ?: "" - if (content.isBlank()) return - decryptedContent = content - } + if (blockList != null) { + PeopleListEvent.addWord( + earlierVersion = blockList, + word = word, + isPrivate = true, + signer = signer + ) { + Client.send(it) + LocalCache.consume(it) - val privateTags = blockList.privateTagsOrEmpty(decryptedContent).plus(element = listOf("word", word)) - val msg = Event.mapper.writeValueAsString(privateTags) - ExternalSignerUtils.encrypt(msg, keyPair.pubKey.toHexKey(), id) - val eventContent = ExternalSignerUtils.content[id] ?: "" - if (eventContent.isBlank()) return - eventContent + live.invalidateData() + saveable.invalidateData() } - - var event = if (blockList != null) { - PeopleListEvent.addWord( - earlierVersion = blockList, - word = word, - isPrivate = true, - pubKey = keyPair.pubKey.toHexKey(), - encryptedContent - ) - } else { - PeopleListEvent.createListWithWord( - name = PeopleListEvent.blockList, - word = word, - isPrivate = true, - pubKey = keyPair.pubKey.toHexKey(), - encryptedContent - ) - } - - ExternalSignerUtils.openSigner(event) - - val eventContent = ExternalSignerUtils.content[event.id] ?: "" - if (eventContent.isBlank()) return - event = PeopleListEvent( - event.id, - event.pubKey, - event.createdAt, - event.tags, - event.content, - eventContent - ) - - Client.send(event) - LocalCache.consume(event) } else { - val event = if (blockList != null) { - PeopleListEvent.addWord( - earlierVersion = blockList, - word = word, - isPrivate = true, - privateKey = keyPair.privKey!! - ) - } else { - PeopleListEvent.createListWithWord( - name = PeopleListEvent.blockList, - word = word, - isPrivate = true, - privateKey = keyPair.privKey!! - ) + PeopleListEvent.createListWithWord( + name = PeopleListEvent.blockList, + word = word, + isPrivate = true, + signer = signer + ) { + Client.send(it) + LocalCache.consume(it) + + live.invalidateData() + saveable.invalidateData() } - - Client.send(event) - LocalCache.consume(event) } - - live.invalidateData() - saveable.invalidateData() } fun showWord(word: String) { - val blockList = migrateHiddenUsersIfNeeded(getBlockList()) + val blockList = getBlockList() if (blockList != null) { - if (loginWithExternalSigner) { - val content = blockList.content - val encryptedContent = if (content.isBlank()) { - val privateTags = listOf(listOf("word", word)) - val msg = Event.mapper.writeValueAsString(privateTags) + PeopleListEvent.removeWord( + earlierVersion = blockList, + word = word, + isPrivate = true, + signer = signer + ) { + Client.send(it) + LocalCache.consume(it) - ExternalSignerUtils.encrypt(msg, keyPair.pubKey.toHexKey(), blockList.id) - val eventContent = ExternalSignerUtils.content[blockList.id] ?: "" - if (eventContent.isBlank()) return - eventContent - } else { - var decryptedContent = ExternalSignerUtils.cachedDecryptedContent[blockList.id] - if (decryptedContent == null) { - ExternalSignerUtils.decrypt(blockList.content, keyPair.pubKey.toHexKey(), blockList.id) - val eventContent = ExternalSignerUtils.content[blockList.id] ?: "" - if (eventContent.isBlank()) return - decryptedContent = eventContent - } - val privateTags = blockList.privateTagsOrEmpty(decryptedContent).minus(element = listOf("word", word)) - val msg = Event.mapper.writeValueAsString(privateTags) - ExternalSignerUtils.encrypt(msg, keyPair.pubKey.toHexKey(), blockList.id) - val eventContent = ExternalSignerUtils.content[blockList.id] ?: "" - if (eventContent.isBlank()) return - eventContent - } - - var event = PeopleListEvent.removeTag( - earlierVersion = blockList, - tag = word, - isPrivate = true, - pubKey = keyPair.pubKey.toHexKey(), - encryptedContent - ) - - ExternalSignerUtils.openSigner(event) - val eventContent = ExternalSignerUtils.content[event.id] ?: "" - if (eventContent.isBlank()) return - event = PeopleListEvent.create(event, eventContent) - - Client.send(event) - LocalCache.consume(event) - } else { - val event = PeopleListEvent.removeWord( - earlierVersion = blockList, - word = word, - isPrivate = true, - privateKey = keyPair.privKey!! - ) - - Client.send(event) - LocalCache.consume(event) + live.invalidateData() + saveable.invalidateData() } } - - transientHiddenUsers = (transientHiddenUsers - word).toImmutableSet() - live.invalidateData() - saveable.invalidateData() } - fun hideUser(pubkeyHex: String) { - val blockList = migrateHiddenUsersIfNeeded(getBlockList()) - if (loginWithExternalSigner) { - val id = blockList?.id - val encryptedContent = if (id == null) { - val privateTags = listOf(listOf("p", pubkeyHex)) - val msg = Event.mapper.writeValueAsString(privateTags) + suspend fun hideUser(pubkeyHex: String) { + val blockList = getBlockList() - ExternalSignerUtils.encrypt(msg, keyPair.pubKey.toHexKey(), "encrypted") - val encryptedContent = ExternalSignerUtils.content["encrypted"] ?: "" - ExternalSignerUtils.content.remove("encrypted") - if (encryptedContent.isBlank()) return - encryptedContent - } else { - var decryptedContent = ExternalSignerUtils.cachedDecryptedContent[id] - if (decryptedContent == null) { - ExternalSignerUtils.decrypt(blockList.content, keyPair.pubKey.toHexKey(), id) - val content = ExternalSignerUtils.content[id] ?: "" - if (content.isBlank()) return - decryptedContent = content - } + if (blockList != null) { + PeopleListEvent.addUser( + earlierVersion = blockList, + pubKeyHex = pubkeyHex, + isPrivate = true, + signer = signer + ) { + Client.send(it) + LocalCache.consume(it) - val privateTags = blockList.privateTagsOrEmpty(decryptedContent).plus(element = listOf("p", pubkeyHex)) - val msg = Event.mapper.writeValueAsString(privateTags) - ExternalSignerUtils.encrypt(msg, keyPair.pubKey.toHexKey(), id) - val eventContent = ExternalSignerUtils.content[id] ?: "" - if (eventContent.isBlank()) return - eventContent + live.invalidateData() + saveable.invalidateData() } - - var event = if (blockList != null) { - PeopleListEvent.addUser( - earlierVersion = blockList, - pubKeyHex = pubkeyHex, - isPrivate = true, - pubKey = keyPair.pubKey.toHexKey(), - encryptedContent - ) - } else { - PeopleListEvent.createListWithUser( - name = PeopleListEvent.blockList, - pubKeyHex = pubkeyHex, - isPrivate = true, - pubKey = keyPair.pubKey.toHexKey(), - encryptedContent - ) - } - - ExternalSignerUtils.openSigner(event) - val eventContent = ExternalSignerUtils.content[event.id] ?: "" - if (eventContent.isBlank()) return - event = PeopleListEvent( - event.id, - event.pubKey, - event.createdAt, - event.tags, - event.content, - eventContent - ) - - Client.send(event) - LocalCache.consume(event) } else { - val event = if (blockList != null) { - PeopleListEvent.addUser( - earlierVersion = blockList, - pubKeyHex = pubkeyHex, - isPrivate = true, - privateKey = keyPair.privKey!! - ) - } else { - PeopleListEvent.createListWithUser( - name = PeopleListEvent.blockList, - pubKeyHex = pubkeyHex, - isPrivate = true, - privateKey = keyPair.privKey!! - ) + PeopleListEvent.createListWithUser( + name = PeopleListEvent.blockList, + pubKeyHex = pubkeyHex, + isPrivate = true, + signer = signer + ) { + Client.send(it) + LocalCache.consume(it) + + live.invalidateData() + saveable.invalidateData() } - - Client.send(event) - LocalCache.consume(event) } - - live.invalidateData() - saveable.invalidateData() } - fun showUser(pubkeyHex: String) { - val blockList = migrateHiddenUsersIfNeeded(getBlockList()) + suspend fun showUser(pubkeyHex: String) { + val blockList = getBlockList() if (blockList != null) { - if (loginWithExternalSigner) { - val content = blockList.content - val encryptedContent = if (content.isBlank()) { - val privateTags = listOf(listOf("p", pubkeyHex)) - val msg = Event.mapper.writeValueAsString(privateTags) + PeopleListEvent.removeUser( + earlierVersion = blockList, + pubKeyHex = pubkeyHex, + isPrivate = true, + signer = signer + ) { + Client.send(it) + LocalCache.consume(it) - ExternalSignerUtils.encrypt(msg, keyPair.pubKey.toHexKey(), blockList.id) - val eventContent = ExternalSignerUtils.content[blockList.id] ?: "" - if (eventContent.isBlank()) return - eventContent - } else { - var decryptedContent = ExternalSignerUtils.cachedDecryptedContent[blockList.id] - if (decryptedContent == null) { - ExternalSignerUtils.decrypt(blockList.content, keyPair.pubKey.toHexKey(), blockList.id) - val eventContent = ExternalSignerUtils.content[blockList.id] ?: "" - if (eventContent.isBlank()) return - decryptedContent = eventContent - } - val privateTags = blockList.privateTagsOrEmpty(decryptedContent).minus(element = listOf("p", pubkeyHex)) - val msg = Event.mapper.writeValueAsString(privateTags) - ExternalSignerUtils.encrypt(msg, keyPair.pubKey.toHexKey(), blockList.id) - val eventContent = ExternalSignerUtils.content[blockList.id] ?: "" - if (eventContent.isBlank()) return - eventContent - } - - var event = PeopleListEvent.removeTag( - earlierVersion = blockList, - tag = pubkeyHex, - isPrivate = true, - pubKey = keyPair.pubKey.toHexKey(), - encryptedContent - ) - - ExternalSignerUtils.openSigner(event) - val eventContent = ExternalSignerUtils.content[event.id] ?: "" - if (eventContent.isBlank()) return - event = PeopleListEvent.create(event, eventContent) - - Client.send(event) - LocalCache.consume(event) - } else { - val event = PeopleListEvent.removeUser( - earlierVersion = blockList, - pubKeyHex = pubkeyHex, - isPrivate = true, - privateKey = keyPair.privKey!! - ) - - Client.send(event) - LocalCache.consume(event) + transientHiddenUsers = (transientHiddenUsers - pubkeyHex).toImmutableSet() + live.invalidateData() + saveable.invalidateData() } } - - transientHiddenUsers = (transientHiddenUsers - pubkeyHex).toImmutableSet() - live.invalidateData() - saveable.invalidateData() } fun changeDefaultZapType(zapType: LnZapEvent.ZapType) { @@ -2629,25 +1545,25 @@ class Account( } fun changeDefaultHomeFollowList(name: String) { - defaultHomeFollowList = name + defaultHomeFollowList.tryEmit(name) live.invalidateData() saveable.invalidateData() } fun changeDefaultStoriesFollowList(name: String) { - defaultStoriesFollowList = name + defaultStoriesFollowList.tryEmit(name) live.invalidateData() saveable.invalidateData() } fun changeDefaultNotificationFollowList(name: String) { - defaultNotificationFollowList = name + defaultNotificationFollowList.tryEmit(name) live.invalidateData() saveable.invalidateData() } fun changeDefaultDiscoveryFollowList(name: String) { - defaultDiscoveryFollowList = name + defaultDiscoveryFollowList.tryEmit(name) live.invalidateData() saveable.invalidateData() } @@ -2670,278 +1586,78 @@ class Account( saveable.invalidateData() } - fun selectedUsersFollowList(listName: String?): Set? { - if (listName == GLOBAL_FOLLOWS) return null - if (listName == KIND3_FOLLOWS) return userProfile().cachedFollowingKeySet() - - val privKey = keyPair.privKey - - return if (listName != null) { - val list = LocalCache.addressables[listName] - if (list != null) { - val publicHexList = (list.event as? PeopleListEvent)?.bookmarkedPeople() ?: emptySet() - val privateHexList = privKey?.let { - (list.event as? PeopleListEvent)?.privateTaggedUsers(it) - } ?: emptySet() - - (publicHexList + privateHexList).toSet() - } else { - emptySet() - } - } else { - emptySet() - } - } - - fun selectedTagsFollowList(listName: String?): Set? { - if (listName == GLOBAL_FOLLOWS) return null - if (listName == KIND3_FOLLOWS) return userProfile().cachedFollowingTagSet() - - val privKey = keyPair.privKey - - return if (listName != null) { - val list = LocalCache.addressables[listName] - if (list != null) { - val publicAddresses = list.event?.hashtags() ?: emptySet() - val privateAddresses = privKey?.let { - (list.event as? GeneralListEvent)?.privateHashtags(it) - } ?: emptySet() - - (publicAddresses + privateAddresses).toSet() - } else { - emptySet() - } - } else { - emptySet() - } - } - - fun selectedGeohashesFollowList(listName: String?): Set? { - if (listName == GLOBAL_FOLLOWS) return null - if (listName == KIND3_FOLLOWS) return userProfile().cachedFollowingGeohashSet() - - val privKey = keyPair.privKey - - return if (listName != null) { - val list = LocalCache.addressables[listName] - if (list != null) { - val publicAddresses = list.event?.geohashes() ?: emptySet() - val privateAddresses = privKey?.let { - (list.event as? GeneralListEvent)?.privateGeohashes(it) - } ?: emptySet() - - (publicAddresses + privateAddresses).toSet() - } else { - emptySet() - } - } else { - emptySet() - } - } - - fun selectedCommunitiesFollowList(listName: String?): Set? { - if (listName == GLOBAL_FOLLOWS) return null - if (listName == KIND3_FOLLOWS) return userProfile().cachedFollowingCommunitiesSet() - - val privKey = keyPair.privKey - - return if (listName != null) { - val list = LocalCache.addressables[listName] - if (list != null) { - val publicAddresses = list.event?.taggedAddresses()?.map { it.toTag() } ?: emptySet() - val privateAddresses = privKey?.let { - (list.event as? GeneralListEvent)?.privateTaggedAddresses(it)?.map { it.toTag() } - } ?: emptySet() - - (publicAddresses + privateAddresses).toSet() - } else { - emptySet() - } - } else { - emptySet() - } - } - fun selectedChatsFollowList(): Set { val contactList = userProfile().latestContactList return contactList?.taggedEvents()?.toSet() ?: DefaultChannels } fun sendChangeChannel(name: String, about: String, picture: String, channel: Channel) { - if (!isWriteable() && !loginWithExternalSigner) return + if (!isWriteable()) return - val metadata = ChannelCreateEvent.ChannelData( + ChannelMetadataEvent.create( name, about, - picture - ) - - var event = ChannelMetadataEvent.create( - newChannelInfo = metadata, + picture, originalChannelIdHex = channel.idHex, - keyPair = keyPair - ) - if (loginWithExternalSigner) { - ExternalSignerUtils.openSigner(event) - val eventContent = ExternalSignerUtils.content[event.id] ?: "" - if (eventContent.isBlank()) return - event = ChannelMetadataEvent.create(event, eventContent) - } + signer = signer + ) { + Client.send(it) + LocalCache.justConsume(it, null) - Client.send(event) - LocalCache.consume(event) - - follow(channel) - } - - fun unwrap(event: GiftWrapEvent): Event? { - if (!isWriteable() && !loginWithExternalSigner) return null - - if (loginWithExternalSigner) { - var decryptedContent = ExternalSignerUtils.cachedDecryptedContent[event.id] - if (decryptedContent == null) { - ExternalSignerUtils.decrypt(event.content, event.pubKey, event.id, SignerType.NIP44_DECRYPT) - } - decryptedContent = ExternalSignerUtils.cachedDecryptedContent[event.id] ?: "" - if (decryptedContent.isEmpty()) return null - return event.cachedGift(keyPair.pubKey, decryptedContent) - } - - return event.cachedGift(keyPair.privKey!!) - } - - fun unseal(event: SealedGossipEvent): Event? { - if (!isWriteable() && !loginWithExternalSigner) return null - - if (loginWithExternalSigner) { - var decryptedContent = ExternalSignerUtils.cachedDecryptedContent[event.id] - if (decryptedContent == null) { - ExternalSignerUtils.decrypt(event.content, event.pubKey, event.id, SignerType.NIP44_DECRYPT) - } - decryptedContent = ExternalSignerUtils.cachedDecryptedContent[event.id] ?: "" - if (decryptedContent.isEmpty()) return null - return event.cachedGossip(keyPair.pubKey, decryptedContent) - } - - return event.cachedGossip(keyPair.privKey!!) - } - - fun decryptContent(note: Note): String? { - return if (loginWithExternalSigner) { - decryptContentWithExternalSigner(note) - } else { - decryptContentInternalSigner(note) + follow(channel) } } - fun decryptContentInternalSigner(note: Note): String? { - val privKey = keyPair.privKey + fun unwrap(event: GiftWrapEvent, onReady: (Event) -> Unit) { + if (!isWriteable()) return + + return event.cachedGift(signer, onReady) + } + + fun unseal(event: SealedGossipEvent, onReady: (Event) -> Unit) { + if (!isWriteable()) return + + return event.cachedGossip(signer, onReady) + } + + fun cachedDecryptContent(note: Note): String? { val event = note.event - return if (event is PrivateDmEvent && privKey != null) { - event.plainContent(privKey, event.talkingWith(userProfile().pubkeyHex).hexToByteArray()) - } else if (event is LnZapRequestEvent && privKey != null) { - decryptZapContentAuthor(note)?.content() + return if (event is PrivateDmEvent && isWriteable()) { + event.cachedContentFor(signer) + } else if (event is LnZapRequestEvent && event.isPrivateZap() && isWriteable()) { + event.cachedPrivateZap()?.content } else { event?.content() } } - fun decryptContentWithExternalSigner(note: Note): String? = with(Dispatchers.IO) { + fun decryptContent(note: Note, onReady: (String) -> Unit) { val event = note.event - return when (event) { - is PrivateDmEvent -> { - if (ExternalSignerUtils.cachedDecryptedContent[event.id] == null) { - ExternalSignerUtils.decryptDM( - event.content, - event.talkingWith(userProfile().pubkeyHex), - event.id - ) - ExternalSignerUtils.cachedDecryptedContent[event.id] - } else { - ExternalSignerUtils.cachedDecryptedContent[event.id] - } + if (event is PrivateDmEvent && isWriteable()) { + event.plainContent(signer, onReady) + } else if (event is LnZapRequestEvent) { + decryptZapContentAuthor(note) { + onReady(it.content) } - is LnZapRequestEvent -> { - decryptZapContentAuthor(note)?.content() - } - else -> { - event?.content() + } else { + event?.content()?.let { + onReady(it) } } } - fun decryptZapContentAuthor(note: Note): Event? { + fun decryptZapContentAuthor(note: Note, onReady: (Event) -> Unit) { val event = note.event - val loggedInPrivateKey = keyPair.privKey - - if (loginWithExternalSigner && event is LnZapRequestEvent && event.isPrivateZap()) { - val decryptedContent = ExternalSignerUtils.cachedDecryptedContent[event.id] - if (decryptedContent != null) { - return try { - Event.fromJson(decryptedContent) - } catch (e: Exception) { - null - } - } - ExternalSignerUtils.decryptZapEvent(event) - return null - } - - return if (event is LnZapRequestEvent && loggedInPrivateKey != null && event.isPrivateZap()) { - val recipientPK = event.zappedAuthor().firstOrNull() - val recipientPost = event.zappedPost().firstOrNull() - if (recipientPK == userProfile().pubkeyHex) { - // if the receiver is logged in, these are the params. - val privateKeyToUse = loggedInPrivateKey - val pubkeyToUse = event.pubKey - - event.getPrivateZapEvent(privateKeyToUse, pubkeyToUse) - } else { - // if the sender is logged in, these are the params - val altPubkeyToUse = recipientPK - val altPrivateKeyToUse = if (recipientPost != null) { - LnZapRequestEvent.createEncryptionPrivateKey( - loggedInPrivateKey.toHexKey(), - recipientPost, - event.createdAt - ) - } else if (recipientPK != null) { - LnZapRequestEvent.createEncryptionPrivateKey( - loggedInPrivateKey.toHexKey(), - recipientPK, - event.createdAt - ) - } else { - null - } - - try { - if (altPrivateKeyToUse != null && altPubkeyToUse != null) { - val altPubKeyFromPrivate = CryptoUtils.pubkeyCreate(altPrivateKeyToUse).toHexKey() - - if (altPubKeyFromPrivate == event.pubKey) { - val result = event.getPrivateZapEvent(altPrivateKeyToUse, altPubkeyToUse) - - if (result == null) { - Log.w( - "Private ZAP Decrypt", - "Fail to decrypt Zap from ${note.author?.toBestDisplayName()} ${note.idNote()}" - ) - } - result - } else { - null - } - } else { - null + if (event is LnZapRequestEvent) { + if (event.isPrivateZap()) { + if (isWriteable()) { + event.decryptPrivateZap(signer) { + onReady(it) } - } catch (e: Exception) { - Log.e("Account", "Failed to create pubkey for ZapRequest ${event.id}", e) - null } + } else { + onReady(event) } - } else { - null } } @@ -3039,14 +1755,7 @@ class Account( fun isHidden(user: User) = isHidden(user.pubkeyHex) fun isHidden(userHex: String): Boolean { - val blockList = getBlockList() - val decryptedContent = blockList?.decryptedContent ?: "" - - if (loginWithExternalSigner) { - if (decryptedContent.isBlank()) return false - return (blockList?.publicAndPrivateUsers(decryptedContent)?.contains(userHex) ?: false) || userHex in transientHiddenUsers - } - return (blockList?.publicAndPrivateUsers(keyPair.privKey)?.contains(userHex) ?: false) || userHex in transientHiddenUsers + return flowHiddenUsers.value.hiddenUsers.contains(userHex) || flowHiddenUsers.value.spammers.contains(userHex) } fun followingKeySet(): Set { @@ -3117,7 +1826,7 @@ class Account( ).toSet() } - fun saveRelayList(value: List) { + suspend fun saveRelayList(value: List) { try { localRelays = value.toSet() return sendNewRelayList(value.associate { it.url to ContactListEvent.ReadWrite(it.read, it.write) }) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 4e9ce8341..20a783453 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -1439,7 +1439,7 @@ object LocalCache { val childrenToBeRemoved = mutableListOf() - val toBeRemoved = account.hiddenUsers.map { userHex -> + val toBeRemoved = account.liveHiddenUsers.value?.hiddenUsers?.map { userHex -> ( notes.values.filter { it.event?.pubKey() == userHex @@ -1447,7 +1447,7 @@ object LocalCache { it.event?.pubKey() == userHex } ).toSet() - }.flatten() + }?.flatten() ?: emptyList() toBeRemoved.forEach { removeFromCache(it) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt index 1ae4df6f3..1d15bf76f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -39,9 +39,11 @@ import com.vitorpamplona.quartz.events.LongTextNoteEvent import com.vitorpamplona.quartz.events.PayInvoiceSuccessResponse import com.vitorpamplona.quartz.events.RepostEvent import com.vitorpamplona.quartz.events.WrappedEvent +import com.vitorpamplona.quartz.signers.NostrSigner import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow import java.math.BigDecimal import java.time.Instant import java.time.ZoneId @@ -152,6 +154,7 @@ open class Note(val idHex: String) { this.replyTo = replyTo liveSet?.innerMetadata?.invalidateData() + flowSet?.metadata?.invalidateData() } } @@ -438,19 +441,68 @@ open class Note(val idHex: String) { } } - fun isZappedBy(user: User, account: Account): Boolean { - // Zaps who the requester was the user - return zaps.any { - it.key.author?.pubkeyHex == user.pubkeyHex || account.decryptZapContentAuthor(it.key)?.pubKey == user.pubkeyHex - } || zapPayments.any { - val zapResponseEvent = it.value?.event as? LnZapPaymentResponseEvent - val response = if (zapResponseEvent != null) { - account.decryptZapPaymentResponseEvent(zapResponseEvent) - } else { - null - } - response is PayInvoiceSuccessResponse && account.isNIP47Author(zapResponseEvent?.requestAuthor()) + private fun recursiveIsPaidByCalculation( + account: Account, + remainingZapPayments: List>, + onWasZappedByAuthor: () -> Unit + ) { + if (remainingZapPayments.isEmpty()) { + return } + + val next = remainingZapPayments.first() + + val zapResponseEvent = next.second?.event as? LnZapPaymentResponseEvent + if (zapResponseEvent != null) { + account.decryptZapPaymentResponseEvent(zapResponseEvent) { response -> + if (response is PayInvoiceSuccessResponse && account.isNIP47Author(zapResponseEvent.requestAuthor())) { + onWasZappedByAuthor() + } else { + recursiveIsPaidByCalculation( + account, + remainingZapPayments.minus(next), + onWasZappedByAuthor + ) + } + } + } + } + + private fun recursiveIsZappedByCalculation( + option: Int?, + user: User, + account: Account, + remainingZapEvents: List>, + onWasZappedByAuthor: () -> Unit + ) { + if (remainingZapEvents.isEmpty()) { + return + } + + val next = remainingZapEvents.first() + + if (next.first.author?.pubkeyHex == user.pubkeyHex) { + onWasZappedByAuthor() + } else { + account.decryptZapContentAuthor(next.first) { + if (it.pubKey == user.pubkeyHex && (option == null || option == (it as? LnZapEvent)?.zappedPollOption())) { + onWasZappedByAuthor() + } else { + recursiveIsZappedByCalculation(option, user, account, remainingZapEvents.minus(next), onWasZappedByAuthor) + } + } + } + } + + fun isZappedBy(user: User, account: Account, onWasZappedByAuthor: () -> Unit) { + recursiveIsZappedByCalculation(null, user, account, zaps.toList(), onWasZappedByAuthor) + if (account.userProfile() == user) { + recursiveIsPaidByCalculation(account, zapPayments.toList(), onWasZappedByAuthor) + } + } + + fun isZappedBy(option: Int?, user: User, account: Account, onWasZappedByAuthor: () -> Unit) { + recursiveIsZappedByCalculation(option, user, account, zaps.toList(), onWasZappedByAuthor) } fun getReactionBy(user: User): String? { @@ -499,49 +551,67 @@ open class Note(val idHex: String) { zapsAmount = sumOfAmounts } - fun zappedAmountWithNWCPayments(privKey: ByteArray?, walletServicePubkey: ByteArray?): BigDecimal { - if (zapPayments.isEmpty()) return zapsAmount - - var sumOfAmounts = zapsAmount - - val invoiceSet = LinkedHashSet(zaps.size + zapPayments.size) - zaps.forEach { - (it.value as? LnZapEvent)?.lnInvoice()?.let { - invoiceSet.add(it) - } + private fun recursiveZappedAmountCalculation( + invoiceSet: LinkedHashSet, + remainingZapPayments: List>, + signer: NostrSigner, + output: BigDecimal, + onReady: (BigDecimal) -> Unit + ) { + if (remainingZapPayments.isEmpty()) { + onReady(output) + return } - if (privKey != null && walletServicePubkey != null) { - zapPayments.forEach { - val noteEvent = (it.value?.event as? LnZapPaymentResponseEvent)?.response( - privKey, - walletServicePubkey - ) - if (noteEvent is PayInvoiceSuccessResponse) { - val invoice = (it.key.event as? LnZapPaymentRequestEvent)?.lnInvoice( - privKey, - walletServicePubkey - ) + val next = remainingZapPayments.first() + (next.second?.event as? LnZapPaymentResponseEvent)?.response(signer) { noteEvent -> + if (noteEvent is PayInvoiceSuccessResponse) { + (next.first.event as? LnZapPaymentRequestEvent)?.lnInvoice(signer) { invoice -> val amount = try { - if (invoice == null) { - null - } else { - LnInvoiceUtil.getAmountInSats(invoice) - } + LnInvoiceUtil.getAmountInSats(invoice) } catch (e: java.lang.Exception) { null } - if (invoice != null && amount != null && !invoiceSet.contains(invoice)) { + var newAmount = output + + if (amount != null && !invoiceSet.contains(invoice)) { invoiceSet.add(invoice) - sumOfAmounts += amount + newAmount += amount } + + recursiveZappedAmountCalculation( + invoiceSet, + remainingZapPayments.minus(next), + signer, + newAmount, + onReady + ) } } } + } - return sumOfAmounts + fun zappedAmountWithNWCPayments(signer: NostrSigner, onReady: (BigDecimal) -> Unit) { + if (zapPayments.isEmpty()) { + onReady(zapsAmount) + } + + val invoiceSet = LinkedHashSet(zaps.size + zapPayments.size) + zaps.forEach { + (it.value?.event as? LnZapEvent)?.lnInvoice()?.let { + invoiceSet.add(it) + } + } + + recursiveZappedAmountCalculation( + invoiceSet, + zapPayments.toList(), + signer, + zapsAmount, + onReady + ) } fun hasPledgeBy(user: User): Boolean { @@ -659,7 +729,32 @@ open class Note(val idHex: String) { lastReactionsDownloadTime = emptyMap() } + fun isHiddenFor(accountChoices: Account.LiveHiddenUsers): Boolean { + val thisEvent = event ?: return false + + val isBoostedNoteHidden = if (thisEvent is GenericRepostEvent || thisEvent is RepostEvent || thisEvent is CommunityPostApprovalEvent) { + replyTo?.lastOrNull()?.isHiddenFor(accountChoices) ?: false + } else { + false + } + + val isHiddenByWord = if (thisEvent is BaseTextNoteEvent) { + accountChoices.hiddenWords.any { + thisEvent.content.contains(it, true) + } + } else { + false + } + + val isSensitive = thisEvent.isSensitive() + return isBoostedNoteHidden || isHiddenByWord || + accountChoices.hiddenUsers.contains(author?.pubkeyHex) || + accountChoices.spammers.contains(author?.pubkeyHex) || + (isSensitive && accountChoices.showSensitiveContent == false) + } + var liveSet: NoteLiveSet? = null + var flowSet: NoteFlowSet? = null @Synchronized fun createOrDestroyLiveSync(create: Boolean) { @@ -688,28 +783,45 @@ open class Note(val idHex: String) { } } - fun isHiddenFor(accountChoices: Account.LiveHiddenUsers): Boolean { - val thisEvent = event ?: return false - - val isBoostedNoteHidden = if (thisEvent is GenericRepostEvent || thisEvent is RepostEvent || thisEvent is CommunityPostApprovalEvent) { - replyTo?.lastOrNull()?.isHiddenFor(accountChoices) ?: false - } else { - false - } - - val isHiddenByWord = if (thisEvent is BaseTextNoteEvent) { - accountChoices.hiddenWords.any { - thisEvent.content.contains(it, true) + @Synchronized + fun createOrDestroyFlowSync(create: Boolean) { + if (create) { + if (flowSet == null) { + flowSet = NoteFlowSet(this) } } else { - false + if (flowSet != null && flowSet?.isInUse() == false) { + flowSet?.destroy() + flowSet = null + } } + } - val isSensitive = thisEvent.isSensitive() - return isBoostedNoteHidden || isHiddenByWord || - accountChoices.hiddenUsers.contains(author?.pubkeyHex) || - accountChoices.spammers.contains(author?.pubkeyHex) || - (isSensitive && accountChoices.showSensitiveContent == false) + fun flow(): NoteFlowSet { + if (flowSet == null) { + createOrDestroyFlowSync(true) + } + return flowSet!! + } + + fun clearFlow() { + if (flowSet != null && flowSet?.isInUse() == false) { + createOrDestroyFlowSync(false) + } + } +} + +@Stable +class NoteFlowSet(u: Note) { + // Observers line up here. + val metadata = NoteBundledRefresherFlow(u) + + fun isInUse(): Boolean { + return metadata.stateFlow.subscriptionCount.value > 0 + } + + fun destroy() { + metadata.destroy() } } @@ -800,6 +912,27 @@ class NoteLiveSet(u: Note) { } } +@Stable +class NoteBundledRefresherFlow(val note: Note) { + // Refreshes observers in batches. + private val bundler = BundledUpdate(500, Dispatchers.IO) + val stateFlow = MutableStateFlow(NoteState(note)) + + fun destroy() { + bundler.cancel() + } + + fun invalidateData() { + checkNotInMainThread() + + bundler.invalidate() { + checkNotInMainThread() + + stateFlow.emit(NoteState(note)) + } + } +} + @Stable class NoteBundledRefresherLiveData(val note: Note) : LiveData(NoteState(note)) { // Refreshes observers in batches. diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt index e2744bbc3..23d942e73 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt @@ -409,7 +409,9 @@ class UserLiveSet(u: User) { val relays = innerRelays.map { it } val relayInfo = innerRelayInfo.map { it } val zaps = innerZaps.map { it } - val bookmarks = innerBookmarks.map { it } + val bookmarks = innerBookmarks.map { + it + } val statuses = innerStatuses.map { it } val profilePictureChanges = innerMetadata.map { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/ExternalSignerUtils.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/ExternalSignerUtils.kt deleted file mode 100644 index 53a2070d5..000000000 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/ExternalSignerUtils.kt +++ /dev/null @@ -1,311 +0,0 @@ -package com.vitorpamplona.amethyst.service - -import android.app.Activity -import android.content.Intent -import android.net.Uri -import android.util.Log -import android.util.LruCache -import android.widget.Toast -import androidx.activity.result.ActivityResultLauncher -import androidx.activity.result.contract.ActivityResultContracts -import com.vitorpamplona.amethyst.Amethyst -import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.ServiceManager -import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.ui.MainActivity -import com.vitorpamplona.quartz.encoders.HexKey -import com.vitorpamplona.quartz.encoders.toNpub -import com.vitorpamplona.quartz.events.EventInterface -import com.vitorpamplona.quartz.events.LnZapRequestEvent -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch - -enum class SignerType { - SIGN_EVENT, - NIP04_ENCRYPT, - NIP04_DECRYPT, - NIP44_ENCRYPT, - NIP44_DECRYPT, - GET_PUBLIC_KEY, - DECRYPT_ZAP_EVENT -} - -object ExternalSignerUtils { - val content = LruCache(10) - var isActivityRunning: Boolean = false - val cachedDecryptedContent = mutableMapOf() - lateinit var account: Account - private lateinit var activityResultLauncher: ActivityResultLauncher - private lateinit var decryptResultLauncher: ActivityResultLauncher - private lateinit var blockListResultLauncher: ActivityResultLauncher - - @OptIn(DelicateCoroutinesApi::class) - fun requestRejectedToast() { - GlobalScope.launch(Dispatchers.Main) { - Toast.makeText( - Amethyst.instance, - Amethyst.instance.getString(R.string.sign_request_rejected), - Toast.LENGTH_SHORT - ).show() - } - } - - @OptIn(DelicateCoroutinesApi::class) - fun default() { - isActivityRunning = false - ServiceManager.shouldPauseService = true - GlobalScope.launch(Dispatchers.IO) { - isActivityRunning = false - ServiceManager.shouldPauseService = true - } - } - - @OptIn(DelicateCoroutinesApi::class) - fun start(activity: MainActivity) { - activityResultLauncher = activity.registerForActivityResult( - ActivityResultContracts.StartActivityForResult() - ) { - if (it.resultCode != Activity.RESULT_OK) { - requestRejectedToast() - } else { - val event = it.data?.getStringExtra("signature") ?: "" - val id = it.data?.getStringExtra("id") ?: "" - if (id.isNotBlank()) { - content.put(id, event) - } - } - default() - } - - decryptResultLauncher = activity.registerForActivityResult( - ActivityResultContracts.StartActivityForResult() - ) { - if (it.resultCode != Activity.RESULT_OK) { - requestRejectedToast() - } else { - val event = it.data?.getStringExtra("signature") ?: "" - val id = it.data?.getStringExtra("id") ?: "" - if (id.isNotBlank()) { - content.put(id, event) - cachedDecryptedContent[id] = event - } - } - default() - } - - blockListResultLauncher = activity.registerForActivityResult( - ActivityResultContracts.StartActivityForResult() - ) { - if (it.resultCode != Activity.RESULT_OK) { - requestRejectedToast() - } else { - val decryptedContent = it.data?.getStringExtra("signature") ?: "" - val id = it.data?.getStringExtra("id") ?: "" - if (id.isNotBlank()) { - cachedDecryptedContent[id] = decryptedContent - account.live.invalidateData() - } - } - default() - } - } - - @OptIn(DelicateCoroutinesApi::class) - fun openSigner( - data: String, - type: SignerType, - intentResult: ActivityResultLauncher, - pubKey: HexKey, - id: String - ) { - try { - ServiceManager.shouldPauseService = false - val intent = Intent(Intent.ACTION_VIEW, Uri.parse("nostrsigner:$data")) - val signerType = when (type) { - SignerType.SIGN_EVENT -> "sign_event" - SignerType.NIP04_ENCRYPT -> "nip04_encrypt" - SignerType.NIP04_DECRYPT -> "nip04_decrypt" - SignerType.NIP44_ENCRYPT -> "nip44_encrypt" - SignerType.NIP44_DECRYPT -> "nip44_decrypt" - SignerType.GET_PUBLIC_KEY -> "get_public_key" - SignerType.DECRYPT_ZAP_EVENT -> "decrypt_zap_event" - } - intent.putExtra("type", signerType) - intent.putExtra("pubKey", pubKey) - intent.putExtra("id", id) - if (type !== SignerType.GET_PUBLIC_KEY) { - intent.putExtra("current_user", account.keyPair.pubKey.toNpub()) - } - intent.`package` = "com.greenart7c3.nostrsigner" - intentResult.launch(intent) - } catch (e: Exception) { - Log.e("Signer", "Error opening Signer app", e) - GlobalScope.launch(Dispatchers.Main) { - Toast.makeText( - Amethyst.instance, - Amethyst.instance.getString(R.string.error_opening_external_signer), - Toast.LENGTH_SHORT - ).show() - } - } - } - - fun openSigner(event: EventInterface, columnName: String = "signature") { - checkNotInMainThread() - - val result = getDataFromResolver(SignerType.SIGN_EVENT, arrayOf(event.toJson(), event.pubKey()), columnName) - if (result == null) { - ServiceManager.shouldPauseService = false - isActivityRunning = true - openSigner( - event.toJson(), - SignerType.SIGN_EVENT, - activityResultLauncher, - "", - event.id() - ) - while (isActivityRunning) { - Thread.sleep(100) - } - } else { - content.put(event.id(), result) - } - } - - fun decryptBlockList(encryptedContent: String, pubKey: HexKey, id: String, signerType: SignerType = SignerType.NIP04_DECRYPT) { - val result = getDataFromResolver(signerType, arrayOf(encryptedContent, pubKey)) - if (result == null) { - isActivityRunning = true - openSigner( - encryptedContent, - signerType, - blockListResultLauncher, - pubKey, - id - ) - } else { - content.put(id, result) - cachedDecryptedContent[id] = result - } - } - - fun getDataFromResolver(signerType: SignerType, data: Array, columnName: String = "signature"): String? { - val localData = if (signerType !== SignerType.GET_PUBLIC_KEY) { - data.toList().plus(account.keyPair.pubKey.toNpub()).toTypedArray() - } else { - data - } - - Amethyst.instance.contentResolver.query( - Uri.parse("content://com.greenart7c3.nostrsigner.$signerType"), - localData, - null, - null, - null - ).use { - if (it == null) { - return null - } - if (it.moveToFirst()) { - val index = it.getColumnIndex(columnName) - if (index < 0) { - Log.d("getDataFromResolver", "column '$columnName' not found") - return null - } - return it.getString(index) - } - } - return null - } - - fun decrypt(encryptedContent: String, pubKey: HexKey, id: String, signerType: SignerType = SignerType.NIP04_DECRYPT) { - val result = getDataFromResolver(signerType, arrayOf(encryptedContent, pubKey)) - if (result == null) { - isActivityRunning = true - openSigner( - encryptedContent, - signerType, - decryptResultLauncher, - pubKey, - id - ) - while (isActivityRunning) { - // do nothing - } - } else { - content.put(id, result) - cachedDecryptedContent[id] = result - } - } - - fun decryptDM(encryptedContent: String, pubKey: HexKey, id: String, signerType: SignerType = SignerType.NIP04_DECRYPT) { - val result = getDataFromResolver(signerType, arrayOf(encryptedContent, pubKey)) - if (result == null) { - openSigner( - encryptedContent, - signerType, - decryptResultLauncher, - pubKey, - id - ) - } else { - content.put(id, result) - cachedDecryptedContent[id] = result - } - } - - fun decryptBookmark(encryptedContent: String, pubKey: HexKey, id: String, signerType: SignerType = SignerType.NIP04_DECRYPT) { - val result = getDataFromResolver(signerType, arrayOf(encryptedContent, pubKey)) - if (result == null) { - openSigner( - encryptedContent, - signerType, - decryptResultLauncher, - pubKey, - id - ) - } else { - content.put(id, result) - cachedDecryptedContent[id] = result - } - } - - fun encrypt(decryptedContent: String, pubKey: HexKey, id: String, signerType: SignerType = SignerType.NIP04_ENCRYPT) { - content.remove(id) - cachedDecryptedContent.remove(id) - val result = getDataFromResolver(signerType, arrayOf(decryptedContent, pubKey)) - if (result == null) { - isActivityRunning = true - openSigner( - decryptedContent, - signerType, - activityResultLauncher, - pubKey, - id - ) - while (isActivityRunning) { - Thread.sleep(100) - } - } else { - content.put(id, result) - } - } - - fun decryptZapEvent(event: LnZapRequestEvent) { - val result = getDataFromResolver(SignerType.DECRYPT_ZAP_EVENT, arrayOf(event.toJson(), event.pubKey)) - if (result == null) { - openSigner( - event.toJson(), - SignerType.DECRYPT_ZAP_EVENT, - decryptResultLauncher, - event.pubKey, - event.id - ) - } else { - content.put(event.id, result) - cachedDecryptedContent[event.id] = result - } - } -} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/HttpClient.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/HttpClient.kt index d9986573e..a39e73226 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/HttpClient.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/HttpClient.kt @@ -1,6 +1,5 @@ package com.vitorpamplona.amethyst.service -import com.vitorpamplona.amethyst.model.Account import okhttp3.OkHttpClient import java.net.InetSocketAddress import java.net.Proxy @@ -19,8 +18,8 @@ object HttpClient { } } - fun start(account: Account?) { - this.internalProxy = account?.proxy + fun start(proxy: Proxy?) { + this.internalProxy = proxy } fun getHttpClient(): OkHttpClient { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt index efcb57eba..ef2d700ac 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt @@ -32,6 +32,7 @@ import com.vitorpamplona.quartz.events.SealedGossipEvent import com.vitorpamplona.quartz.events.StatusEvent import com.vitorpamplona.quartz.events.TextNoteEvent +// TODO: Migrate this to a property of AccountVi object NostrAccountDataSource : NostrDataSource("AccountData") { lateinit var account: Account @@ -99,7 +100,7 @@ object NostrAccountDataSource : NostrDataSource("AccountData") { filter = JsonFilter( kinds = listOf(ReportEvent.kind), authors = listOf(account.userProfile().pubkeyHex), - since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultNotificationFollowList)?.relayList + since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultNotificationFollowList.value)?.relayList ) ) } @@ -131,7 +132,7 @@ object NostrAccountDataSource : NostrDataSource("AccountData") { ), tags = mapOf("p" to listOf(account.userProfile().pubkeyHex)), limit = 4000, - since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultNotificationFollowList)?.relayList + since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultNotificationFollowList.value)?.relayList ) ) @@ -145,7 +146,7 @@ object NostrAccountDataSource : NostrDataSource("AccountData") { val accountChannel = requestNewChannel { time, relayUrl -> if (hasLoadedTheBasics[account.userProfile()] != null) { - latestEOSEs.addOrUpdate(account.userProfile(), account.defaultNotificationFollowList, relayUrl, time) + latestEOSEs.addOrUpdate(account.userProfile(), account.defaultNotificationFollowList.value, relayUrl, time) } else { hasLoadedTheBasics[account.userProfile()] = true @@ -158,51 +159,21 @@ object NostrAccountDataSource : NostrDataSource("AccountData") { if (LocalCache.justVerify(event)) { if (event is GiftWrapEvent) { - val privateKey = account.keyPair.privKey - if (privateKey != null) { - event.cachedGift(privateKey)?.let { - this.consume(it, relay) - } - } else if (account.loginWithExternalSigner) { - var cached = ExternalSignerUtils.cachedDecryptedContent[event.id] - if (cached == null) { - ExternalSignerUtils.decrypt( - event.content, - event.pubKey, - event.id, - SignerType.NIP44_DECRYPT - ) - cached = ExternalSignerUtils.cachedDecryptedContent[event.id] ?: "" - } - event.cachedGift(account.keyPair.pubKey, cached)?.let { - this.consume(it, relay) - } + // Avoid decrypting over and over again if the event already exist. + if (LocalCache.getNoteIfExists(event.id) != null) return + + event.cachedGift(account.signer) { + this.consume(it, relay) } } if (event is SealedGossipEvent) { - val privateKey = account.keyPair.privKey - if (privateKey != null) { - event.cachedGossip(privateKey)?.let { - LocalCache.justConsume(it, relay) - } - } else if (account.loginWithExternalSigner) { - var cached = ExternalSignerUtils.cachedDecryptedContent[event.id] - if (cached == null) { - ExternalSignerUtils.decrypt( - event.content, - event.pubKey, - event.id, - SignerType.NIP44_DECRYPT - ) - cached = ExternalSignerUtils.cachedDecryptedContent[event.id] ?: "" - } - event.cachedGossip(account.keyPair.pubKey, cached)?.let { - LocalCache.justConsume(it, relay) - } - } + // Avoid decrypting over and over again if the event already exist. + if (LocalCache.getNoteIfExists(event.id) != null) return - // Don't store sealed gossips to avoid rebroadcasting by mistake. + event.cachedGossip(account.signer) { + LocalCache.justConsume(it, relay) + } } else { LocalCache.justConsume(event, relay) } @@ -225,11 +196,13 @@ object NostrAccountDataSource : NostrDataSource("AccountData") { LocalCache.getNoteIfExists(noteEvent.id())?.addRelay(relay) if (noteEvent is GiftWrapEvent) { - val gift = noteEvent.cachedGift(privKey) ?: return - markInnerAsSeenOnRelay(gift, privKey, relay) + noteEvent.cachedGift(account.signer) { gift -> + markInnerAsSeenOnRelay(gift, privKey, relay) + } } else if (noteEvent is SealedGossipEvent) { - val rumor = noteEvent.cachedGossip(privKey) ?: return - markInnerAsSeenOnRelay(rumor, privKey, relay) + noteEvent.cachedGossip(account.signer) { rumor -> + markInnerAsSeenOnRelay(rumor, privKey, relay) + } } } @@ -262,10 +235,9 @@ object NostrAccountDataSource : NostrDataSource("AccountData") { super.auth(relay, challenge) if (this::account.isInitialized) { - val event = account.createAuthEvent(relay, challenge) - if (event != null) { + account.createAuthEvent(relay, challenge) { Client.send( - event, + it, relay.url ) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDiscoveryDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDiscoveryDataSource.kt index ed4c02364..3a6928393 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDiscoveryDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDiscoveryDataSource.kt @@ -1,5 +1,6 @@ package com.vitorpamplona.amethyst.service +import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.relays.EOSEAccount import com.vitorpamplona.amethyst.service.relays.FeedType @@ -12,14 +13,35 @@ import com.vitorpamplona.quartz.events.CommunityDefinitionEvent import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.events.LiveActivitiesEvent +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") { lateinit var account: Account + val scope = Amethyst.instance.applicationIOScope val latestEOSEs = EOSEAccount() + var job: Job? = null + + override fun start() { + job?.cancel() + job = scope.launch(Dispatchers.IO) { + account.liveDiscoveryFollowLists.collect { + invalidateFilters() + } + } + super.start() + } + + override fun stop() { + super.stop() + job?.cancel() + } + fun createLiveStreamFilter(): List { - val follows = account.selectedUsersFollowList(account.defaultDiscoveryFollowList)?.toList() + val follows = account.liveDiscoveryFollowLists.value?.users?.toList() return listOfNotNull( TypedFilter( @@ -28,7 +50,7 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") { authors = follows, kinds = listOf(LiveActivitiesChatMessageEvent.kind, LiveActivitiesEvent.kind), limit = 300, - since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList + since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList.value)?.relayList ) ), follows?.let { @@ -38,7 +60,7 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") { tags = mapOf("p" to it), kinds = listOf(LiveActivitiesEvent.kind), limit = 100, - since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList + since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList.value)?.relayList ) ) } @@ -46,7 +68,7 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") { } fun createPublicChatFilter(): List { - val follows = account.selectedUsersFollowList(account.defaultDiscoveryFollowList)?.toList() + val follows = account.liveDiscoveryFollowLists.value?.users?.toList() val followChats = account.selectedChatsFollowList().toList() return listOf( @@ -56,7 +78,7 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") { authors = follows, kinds = listOf(ChannelCreateEvent.kind, ChannelMetadataEvent.kind, ChannelMessageEvent.kind), limit = 300, - since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList + since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList.value)?.relayList ) ), TypedFilter( @@ -65,14 +87,14 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") { ids = followChats, kinds = listOf(ChannelCreateEvent.kind), limit = 300, - since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList + since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList.value)?.relayList ) ) ) } fun createCommunitiesFilter(): TypedFilter { - val follows = account.selectedUsersFollowList(account.defaultDiscoveryFollowList)?.toList() + val follows = account.liveDiscoveryFollowLists.value?.users?.toList() return TypedFilter( types = setOf(FeedType.GLOBAL), @@ -80,13 +102,13 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") { authors = follows, kinds = listOf(CommunityDefinitionEvent.kind, CommunityPostApprovalEvent.kind), limit = 300, - since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList + since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList.value)?.relayList ) ) } fun createLiveStreamTagsFilter(): TypedFilter? { - val hashToLoad = account.selectedTagsFollowList(account.defaultDiscoveryFollowList) + val hashToLoad = account.liveDiscoveryFollowLists.value?.hashtags?.toList() if (hashToLoad.isNullOrEmpty()) return null @@ -100,13 +122,13 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") { }.flatten() ), limit = 300, - since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList + since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList.value)?.relayList ) ) } fun createLiveStreamGeohashesFilter(): TypedFilter? { - val hashToLoad = account.selectedGeohashesFollowList(account.defaultDiscoveryFollowList) + val hashToLoad = account.liveDiscoveryFollowLists.value?.geotags?.toList() if (hashToLoad.isNullOrEmpty()) return null @@ -120,13 +142,13 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") { }.flatten() ), limit = 300, - since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList + since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList.value)?.relayList ) ) } fun createPublicChatsTagsFilter(): TypedFilter? { - val hashToLoad = account.selectedTagsFollowList(account.defaultDiscoveryFollowList) + val hashToLoad = account.liveDiscoveryFollowLists.value?.hashtags?.toList() if (hashToLoad.isNullOrEmpty()) return null @@ -140,13 +162,13 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") { }.flatten() ), limit = 300, - since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList + since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList.value)?.relayList ) ) } fun createPublicChatsGeohashesFilter(): TypedFilter? { - val hashToLoad = account.selectedGeohashesFollowList(account.defaultDiscoveryFollowList) + val hashToLoad = account.liveDiscoveryFollowLists.value?.geotags?.toList() if (hashToLoad.isNullOrEmpty()) return null @@ -160,13 +182,13 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") { }.flatten() ), limit = 300, - since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList + since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList.value)?.relayList ) ) } fun createCommunitiesTagsFilter(): TypedFilter? { - val hashToLoad = account.selectedTagsFollowList(account.defaultDiscoveryFollowList) + val hashToLoad = account.liveDiscoveryFollowLists.value?.hashtags?.toList() if (hashToLoad.isNullOrEmpty()) return null @@ -180,13 +202,13 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") { }.flatten() ), limit = 300, - since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList + since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList.value)?.relayList ) ) } fun createCommunitiesGeohashesFilter(): TypedFilter? { - val hashToLoad = account.selectedGeohashesFollowList(account.defaultDiscoveryFollowList) + val hashToLoad = account.liveDiscoveryFollowLists.value?.geotags?.toList() if (hashToLoad.isNullOrEmpty()) return null @@ -200,13 +222,13 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") { }.flatten() ), limit = 300, - since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList + since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList.value)?.relayList ) ) } val discoveryFeedChannel = requestNewChannel() { time, relayUrl -> - latestEOSEs.addOrUpdate(account.userProfile(), account.defaultDiscoveryFollowList, relayUrl, time) + latestEOSEs.addOrUpdate(account.userProfile(), account.defaultDiscoveryFollowList.value, relayUrl, time) } override fun updateChannelFilters() { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt index e69d8651c..bd95dd06d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt @@ -1,7 +1,7 @@ package com.vitorpamplona.amethyst.service +import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.UserState import com.vitorpamplona.amethyst.service.relays.EOSEAccount import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.JsonFilter @@ -19,42 +19,35 @@ import com.vitorpamplona.quartz.events.PinListEvent import com.vitorpamplona.quartz.events.PollNoteEvent import com.vitorpamplona.quartz.events.RepostEvent import com.vitorpamplona.quartz.events.TextNoteEvent -import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.Job import kotlinx.coroutines.launch object NostrHomeDataSource : NostrDataSource("HomeFeed") { lateinit var account: Account + val scope = Amethyst.instance.applicationIOScope val latestEOSEs = EOSEAccount() - private val cacheListener: (UserState) -> Unit = { - invalidateFilters() - } + var job: Job? = null - @OptIn(DelicateCoroutinesApi::class) override fun start() { - if (this::account.isInitialized) { - GlobalScope.launch(Dispatchers.Main) { - account.userProfile().live().follows.observeForever(cacheListener) + job?.cancel() + job = account.scope.launch(Dispatchers.IO) { + account.liveHomeFollowLists.collect { + invalidateFilters() } } super.start() } - @OptIn(DelicateCoroutinesApi::class) override fun stop() { super.stop() - if (this::account.isInitialized) { - GlobalScope.launch(Dispatchers.Main) { - account.userProfile().live().follows.removeObserver(cacheListener) - } - } + job?.cancel() } fun createFollowAccountsFilter(): TypedFilter { - val follows = account.selectedUsersFollowList(account.defaultHomeFollowList) + val follows = account.liveHomeFollowLists.value?.users val followSet = follows?.plus(account.userProfile().pubkeyHex)?.toList()?.ifEmpty { null } return TypedFilter( @@ -76,13 +69,13 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") { ), authors = followSet, limit = 400, - since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultHomeFollowList)?.relayList + since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultHomeFollowList.value)?.relayList ) ) } fun createFollowTagsFilter(): TypedFilter? { - val hashToLoad = account.selectedTagsFollowList(account.defaultHomeFollowList) ?: emptySet() + val hashToLoad = account.liveHomeFollowLists.value?.hashtags ?: return null if (hashToLoad.isEmpty()) return null @@ -96,13 +89,13 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") { }.flatten() ), limit = 100, - since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultHomeFollowList)?.relayList + since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultHomeFollowList.value)?.relayList ) ) } fun createFollowGeohashesFilter(): TypedFilter? { - val hashToLoad = account.selectedGeohashesFollowList(account.defaultHomeFollowList) ?: emptySet() + val hashToLoad = account.liveHomeFollowLists.value?.geotags ?: return null if (hashToLoad.isEmpty()) return null @@ -116,13 +109,13 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") { }.flatten() ), limit = 100, - since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultHomeFollowList)?.relayList + since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultHomeFollowList.value)?.relayList ) ) } fun createFollowCommunitiesFilter(): TypedFilter? { - val communitiesToLoad = account.selectedCommunitiesFollowList(account.defaultHomeFollowList) ?: emptySet() + val communitiesToLoad = account.liveHomeFollowLists.value?.communities ?: return null if (communitiesToLoad.isEmpty()) return null @@ -143,13 +136,13 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") { "a" to communitiesToLoad.toList() ), limit = 100, - since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultHomeFollowList)?.relayList + since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultHomeFollowList.value)?.relayList ) ) } val followAccountChannel = requestNewChannel { time, relayUrl -> - latestEOSEs.addOrUpdate(account.userProfile(), account.defaultHomeFollowList, relayUrl, time) + latestEOSEs.addOrUpdate(account.userProfile(), account.defaultHomeFollowList.value, relayUrl, time) } override fun updateChannelFilters() { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrLnZapPaymentResponseDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrLnZapPaymentResponseDataSource.kt index 014a2b4d7..7fb698619 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrLnZapPaymentResponseDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrLnZapPaymentResponseDataSource.kt @@ -7,12 +7,13 @@ import com.vitorpamplona.amethyst.service.relays.Relay import com.vitorpamplona.amethyst.service.relays.TypedFilter import com.vitorpamplona.quartz.events.LnZapPaymentResponseEvent import com.vitorpamplona.quartz.events.RelayAuthEvent +import com.vitorpamplona.quartz.signers.NostrSigner class NostrLnZapPaymentResponseDataSource( private val fromServiceHex: String, private val toUserHex: String, private val replyingToHex: String, - private val authSigningKey: ByteArray + private val authSigner: NostrSigner ) : NostrDataSource("LnZapPaymentResponseFeed") { val feedTypes = setOf(FeedType.WALLET_CONNECT) @@ -44,10 +45,11 @@ class NostrLnZapPaymentResponseDataSource( override fun auth(relay: Relay, challenge: String) { super.auth(relay, challenge) - val event = RelayAuthEvent.create(relay.url, challenge, "", authSigningKey) - Client.send( - event, - relay.url - ) + RelayAuthEvent.create(relay.url, challenge, authSigner) { + Client.send( + it, + relay.url + ) + } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSearchEventOrUserDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSearchEventOrUserDataSource.kt index 6c4492014..fa9dec7f8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSearchEventOrUserDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSearchEventOrUserDataSource.kt @@ -5,6 +5,7 @@ import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.JsonFilter import com.vitorpamplona.amethyst.service.relays.TypedFilter import com.vitorpamplona.quartz.encoders.Hex +import com.vitorpamplona.quartz.encoders.HexValidator import com.vitorpamplona.quartz.encoders.Nip19 import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.events.AudioHeaderEvent @@ -36,7 +37,13 @@ object NostrSearchEventOrUserDataSource : NostrDataSource("SearchEventFeed") { } val hexToWatch = try { - Nip19.uriToRoute(mySearchString)?.hex ?: Hex.decode(mySearchString).toHexKey() + val isAStraightHex = if (HexValidator.isHex(mySearchString)) { + Hex.decode(mySearchString).toHexKey() + } else { + null + } + + Nip19.uriToRoute(mySearchString)?.hex ?: isAStraightHex } catch (e: Exception) { null } @@ -108,11 +115,14 @@ object NostrSearchEventOrUserDataSource : NostrDataSource("SearchEventFeed") { } fun search(searchString: String) { + println("DataSource: ${this.javaClass.simpleName} Search for $searchString") this.searchString = searchString invalidateFilters() } fun clear() { + println("DataSource: ${this.javaClass.simpleName} Clear") searchString = null + invalidateFilters() } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrVideoDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrVideoDataSource.kt index 2fcd5ecb0..7b7e0b002 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrVideoDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrVideoDataSource.kt @@ -1,5 +1,6 @@ package com.vitorpamplona.amethyst.service +import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.relays.EOSEAccount import com.vitorpamplona.amethyst.service.relays.FeedType @@ -7,14 +8,35 @@ import com.vitorpamplona.amethyst.service.relays.JsonFilter import com.vitorpamplona.amethyst.service.relays.TypedFilter import com.vitorpamplona.quartz.events.FileHeaderEvent import com.vitorpamplona.quartz.events.FileStorageHeaderEvent +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch object NostrVideoDataSource : NostrDataSource("VideoFeed") { lateinit var account: Account + val scope = Amethyst.instance.applicationIOScope val latestEOSEs = EOSEAccount() - fun createContextualFilter(): TypedFilter? { - val follows = account.selectedUsersFollowList(account.defaultStoriesFollowList)?.toList() + var job: Job? = null + + override fun start() { + job?.cancel() + job = scope.launch(Dispatchers.IO) { + account.liveStoriesFollowLists.collect { + invalidateFilters() + } + } + super.start() + } + + override fun stop() { + super.stop() + job?.cancel() + } + + fun createContextualFilter(): TypedFilter { + val follows = account.liveStoriesFollowLists.value?.users?.toList() return TypedFilter( types = setOf(FeedType.GLOBAL), @@ -22,15 +44,15 @@ object NostrVideoDataSource : NostrDataSource("VideoFeed") { authors = follows, kinds = listOf(FileHeaderEvent.kind, FileStorageHeaderEvent.kind), limit = 200, - since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultStoriesFollowList)?.relayList + since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultStoriesFollowList.value)?.relayList ) ) } fun createFollowTagsFilter(): TypedFilter? { - val hashToLoad = account.selectedTagsFollowList(account.defaultStoriesFollowList) + val hashToLoad = account.liveStoriesFollowLists.value?.hashtags?.toList() ?: return null - if (hashToLoad.isNullOrEmpty()) return null + if (hashToLoad.isEmpty()) return null return TypedFilter( types = setOf(FeedType.GLOBAL), @@ -42,13 +64,13 @@ object NostrVideoDataSource : NostrDataSource("VideoFeed") { }.flatten() ), limit = 100, - since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultStoriesFollowList)?.relayList + since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultStoriesFollowList.value)?.relayList ) ) } fun createFollowGeohashesFilter(): TypedFilter? { - val hashToLoad = account.selectedGeohashesFollowList(account.defaultStoriesFollowList) + val hashToLoad = account.liveStoriesFollowLists.value?.geotags?.toList() ?: return null if (hashToLoad.isNullOrEmpty()) return null @@ -62,13 +84,13 @@ object NostrVideoDataSource : NostrDataSource("VideoFeed") { }.flatten() ), limit = 100, - since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultStoriesFollowList)?.relayList + since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultStoriesFollowList.value)?.relayList ) ) } val videoFeedChannel = requestNewChannel() { time, relayUrl -> - latestEOSEs.addOrUpdate(account.userProfile(), account.defaultStoriesFollowList, relayUrl, time) + latestEOSEs.addOrUpdate(account.userProfile(), account.defaultStoriesFollowList.value, relayUrl, time) } override fun updateChannelFilters() { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/ZapPaymentHandler.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/ZapPaymentHandler.kt index 7ce89d610..86b42fd70 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/ZapPaymentHandler.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/ZapPaymentHandler.kt @@ -148,6 +148,23 @@ class ZapPaymentHandler(val account: Account) { } } + private fun prepareZapRequestIfNeeded( + note: Note, + pollOption: Int?, + message: String, + zapType: LnZapEvent.ZapType, + overrideUser: User? = null, + onReady: (String?) -> Unit + ) { + if (zapType != LnZapEvent.ZapType.NONZAP) { + account.createZapRequestFor(note, pollOption, message, zapType, overrideUser) { zapRequest -> + onReady(zapRequest.toJson()) + } + } else { + onReady(null) + } + } + private suspend fun innerZap( lud16: String, note: Note, @@ -161,54 +178,49 @@ class ZapPaymentHandler(val account: Account) { zapType: LnZapEvent.ZapType, overrideUser: User? = null ) { - var zapRequestJson = "" + onProgress(0.05f) - if (zapType != LnZapEvent.ZapType.NONZAP) { - val zapRequest = account.createZapRequestFor(note, pollOption, message, zapType, overrideUser) - if (zapRequest != null) { - zapRequestJson = zapRequest.toJson() - } - } + prepareZapRequestIfNeeded(note, pollOption, message, zapType, overrideUser) { zapRequestJson -> + onProgress(0.10f) - onProgress(0.10f) - - LightningAddressResolver().lnAddressInvoice( - lud16, - amount, - message, - zapRequestJson, - onSuccess = { - onProgress(0.7f) - if (account.hasWalletConnectSetup()) { - account.sendZapPaymentRequestFor( - bolt11 = it, - note, - onResponse = { response -> - if (response is PayInvoiceErrorResponse) { - onProgress(0.0f) - onError( - context.getString(R.string.error_dialog_pay_invoice_error), - context.getString( - R.string.wallet_connect_pay_invoice_error_error, - response.error?.message - ?: response.error?.code?.toString() - ?: "Error parsing error message" + LightningAddressResolver().lnAddressInvoice( + lud16, + amount, + message, + zapRequestJson, + onSuccess = { + onProgress(0.7f) + if (account.hasWalletConnectSetup()) { + account.sendZapPaymentRequestFor( + bolt11 = it, + note, + onResponse = { response -> + if (response is PayInvoiceErrorResponse) { + onProgress(0.0f) + onError( + context.getString(R.string.error_dialog_pay_invoice_error), + context.getString( + R.string.wallet_connect_pay_invoice_error_error, + response.error?.message + ?: response.error?.code?.toString() + ?: "Error parsing error message" + ) ) - ) - } else { - onProgress(1f) + } else { + onProgress(1f) + } } - } - ) - onProgress(0.8f) - } else { - onPayInvoiceThroughIntent(it) - onProgress(0f) - } - }, - onError = onError, - onProgress = onProgress, - context = context - ) + ) + onProgress(0.8f) + } else { + onPayInvoiceThroughIntent(it) + onProgress(0f) + } + }, + onError = onError, + onProgress = onProgress, + context = context + ) + } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt index a89e22b84..30979061a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt @@ -9,8 +9,6 @@ import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.quartz.encoders.LnInvoiceUtil import com.vitorpamplona.quartz.encoders.Lud06 import com.vitorpamplona.quartz.encoders.toLnUrl -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext import okhttp3.Request import java.math.BigDecimal import java.math.RoundingMode @@ -33,12 +31,12 @@ class LightningAddressResolver() { return null } - private suspend fun fetchLightningAddressJson( + private fun fetchLightningAddressJson( lnaddress: String, - onSuccess: suspend (String) -> Unit, + onSuccess: (String) -> Unit, onError: (String, String) -> Unit, context: Context - ) = withContext(Dispatchers.IO) { + ) { checkNotInMainThread() val url = assembleUrl(lnaddress) @@ -51,7 +49,7 @@ class LightningAddressResolver() { lnaddress ) ) - return@withContext + return } try { @@ -88,15 +86,17 @@ class LightningAddressResolver() { } } - suspend fun fetchLightningInvoice( + fun fetchLightningInvoice( lnCallback: String, milliSats: Long, message: String, nostrRequest: String? = null, - onSuccess: suspend (String) -> Unit, + onSuccess: (String) -> Unit, onError: (String, String) -> Unit, context: Context - ) = withContext(Dispatchers.IO) { + ) { + checkNotInMainThread() + val encodedMessage = URLEncoder.encode(message, "utf-8") val urlBinder = if (lnCallback.contains("?")) "&" else "?" @@ -124,7 +124,7 @@ class LightningAddressResolver() { } } - suspend fun lnAddressToLnUrl(lnaddress: String, onSuccess: (String) -> Unit, onError: (String, String) -> Unit, context: Context) { + fun lnAddressToLnUrl(lnaddress: String, onSuccess: (String) -> Unit, onError: (String, String) -> Unit, context: Context) { fetchLightningAddressJson( lnaddress, onSuccess = { @@ -135,12 +135,12 @@ class LightningAddressResolver() { ) } - suspend fun lnAddressInvoice( + fun lnAddressInvoice( lnaddress: String, milliSats: Long, message: String, nostrRequest: String? = null, - onSuccess: suspend (String) -> Unit, + onSuccess: (String) -> Unit, onError: (String, String) -> Unit, onProgress: (percent: Float) -> Unit, context: Context diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt index bdc63eacc..bc915fa10 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt @@ -7,8 +7,6 @@ import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.service.ExternalSignerUtils -import com.vitorpamplona.amethyst.service.SignerType import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.sendDMNotification import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.sendZapNotification import com.vitorpamplona.amethyst.ui.note.showAmount @@ -42,110 +40,40 @@ class EventNotificationConsumer(private val applicationContext: Context) { } private suspend fun consumeIfMatchesAccount(pushWrappedEvent: GiftWrapEvent, account: Account) { - val key = account.keyPair.privKey - if (account.loginWithExternalSigner) { - ExternalSignerUtils.account = account - var cached = ExternalSignerUtils.cachedDecryptedContent[pushWrappedEvent.id] - if (cached == null) { - ExternalSignerUtils.decrypt( - pushWrappedEvent.content, - pushWrappedEvent.pubKey, - pushWrappedEvent.id, - SignerType.NIP44_DECRYPT - ) - cached = ExternalSignerUtils.cachedDecryptedContent[pushWrappedEvent.id] ?: "" - } - pushWrappedEvent.unwrap(cached)?.let { notificationEvent -> - if (!LocalCache.justVerify(notificationEvent)) return // invalid event - if (LocalCache.notes[notificationEvent.id] != null) return // already processed + pushWrappedEvent.cachedGift(account.signer) { notificationEvent -> + LocalCache.justConsume(notificationEvent, null) - LocalCache.justConsume(notificationEvent, null) - - unwrapAndConsume(notificationEvent, account)?.let { innerEvent -> - if (innerEvent is PrivateDmEvent) { - notify(innerEvent, account) - } else if (innerEvent is LnZapEvent) { - notify(innerEvent, account) - } else if (innerEvent is ChatMessageEvent) { - notify(innerEvent, account) - } - } - } - } else if (key != null) { - pushWrappedEvent.unwrap(key)?.let { notificationEvent -> - LocalCache.justConsume(notificationEvent, null) - - unwrapAndConsume(notificationEvent, account)?.let { innerEvent -> - if (innerEvent is PrivateDmEvent) { - notify(innerEvent, account) - } else if (innerEvent is LnZapEvent) { - notify(innerEvent, account) - } else if (innerEvent is ChatMessageEvent) { - notify(innerEvent, account) - } + unwrapAndConsume(notificationEvent, account) { innerEvent -> + if (innerEvent is PrivateDmEvent) { + notify(innerEvent, account) + } else if (innerEvent is LnZapEvent) { + notify(innerEvent, account) + } else if (innerEvent is ChatMessageEvent) { + notify(innerEvent, account) } } } } - private fun unwrapAndConsume(event: Event, account: Account): Event? { - if (!LocalCache.justVerify(event)) return null + private fun unwrapAndConsume(event: Event, account: Account, onReady: (Event) -> Unit) { + if (!LocalCache.justVerify(event)) return - return when (event) { + when (event) { is GiftWrapEvent -> { - val key = account.keyPair.privKey - if (key != null) { - event.cachedGift(key)?.let { - unwrapAndConsume(it, account) - } - } else if (account.loginWithExternalSigner) { - var cached = ExternalSignerUtils.cachedDecryptedContent[event.id] - if (cached == null) { - ExternalSignerUtils.decrypt( - event.content, - event.pubKey, - event.id, - SignerType.NIP44_DECRYPT - ) - cached = ExternalSignerUtils.cachedDecryptedContent[event.id] ?: "" - } - event.cachedGift(account.keyPair.pubKey, cached)?.let { - unwrapAndConsume(it, account) - } - } else { - null + event.cachedGift(account.signer) { + unwrapAndConsume(it, account, onReady) } } is SealedGossipEvent -> { - val key = account.keyPair.privKey - if (key != null) { - event.cachedGossip(key)?.let { - // this is not verifiable - LocalCache.justConsume(it, null) - it - } - } else if (account.loginWithExternalSigner) { - var cached = ExternalSignerUtils.cachedDecryptedContent[event.id] - if (cached == null) { - ExternalSignerUtils.decrypt( - event.content, - event.pubKey, - event.id, - SignerType.NIP44_DECRYPT - ) - cached = ExternalSignerUtils.cachedDecryptedContent[event.id] ?: "" - } - event.cachedGossip(account.keyPair.pubKey, cached)?.let { - LocalCache.justConsume(it, null) - it - } - } else { - null + event.cachedGossip(account.signer) { + // this is not verifiable + LocalCache.justConsume(it, null) + onReady(it) } } else -> { LocalCache.justConsume(event, null) - event + onReady(event) } } } @@ -200,11 +128,12 @@ class EventNotificationConsumer(private val applicationContext: Context) { note.author?.let { if (ChatroomKey(persistentSetOf(it.pubkeyHex)) in knownChatrooms) { - val content = acc.decryptContent(note) ?: "" - val user = note.author?.toBestDisplayName() ?: "" - val userPicture = note.author?.profilePicture() - val noteUri = note.toNEvent() - notificationManager().sendDMNotification(event.id, content, user, userPicture, noteUri, applicationContext) + acc.decryptContent(note) { content -> + val user = note.author?.toBestDisplayName() ?: "" + val userPicture = note.author?.profilePicture() + val noteUri = note.toNEvent() + notificationManager().sendDMNotification(event.id, content, user, userPicture, noteUri, applicationContext) + } } } } @@ -217,39 +146,35 @@ class EventNotificationConsumer(private val applicationContext: Context) { if (event.createdAt < TimeUtils.fiveMinutesAgo()) return val noteZapRequest = event.zapRequest?.id?.let { LocalCache.checkGetOrCreateNote(it) } ?: return - val noteZapped = event.zappedPost().firstOrNull()?.let { LocalCache.checkGetOrCreateNote(it) } + val noteZapped = event.zappedPost().firstOrNull()?.let { LocalCache.checkGetOrCreateNote(it) } ?: return if ((event.amount ?: BigDecimal.ZERO) < BigDecimal.TEN) return if (acc.userProfile().pubkeyHex == event.zappedAuthor().firstOrNull()) { val amount = showAmount(event.amount) - val senderInfo = (noteZapRequest.event as? LnZapRequestEvent)?.let { - val decryptedContent = acc.decryptZapContentAuthor(noteZapRequest) - if (decryptedContent != null) { - val author = LocalCache.getOrCreateUser(decryptedContent.pubKey) - Pair(author, decryptedContent.content) - } else if (!noteZapRequest.event?.content().isNullOrBlank()) { - Pair(noteZapRequest.author, noteZapRequest.event?.content()) - } else { - Pair(noteZapRequest.author, null) + (noteZapRequest.event as? LnZapRequestEvent)?.let { event -> + acc.decryptZapContentAuthor(noteZapRequest) { + val author = LocalCache.getOrCreateUser(it.pubKey) + val senderInfo = Pair(author, it.content.ifBlank { null }) + + acc.decryptContent(noteZapped) { + val zappedContent = it.split("\n").get(0) + + val user = senderInfo.first.toBestDisplayName() + var title = applicationContext.getString(R.string.app_notification_zaps_channel_message, amount) + senderInfo.second?.ifBlank { null }?.let { + title += " ($it)" + } + var content = applicationContext.getString(R.string.app_notification_zaps_channel_message_from, user) + zappedContent?.let { + content += " " + applicationContext.getString(R.string.app_notification_zaps_channel_message_for, zappedContent) + } + val userPicture = senderInfo?.first?.profilePicture() + val noteUri = "nostr:Notifications" + notificationManager().sendZapNotification(event.id, content, title, userPicture, noteUri, applicationContext) + } } } - - val zappedContent = - noteZapped?.let { it1 -> acc.decryptContent(it1)?.split("\n")?.get(0) } - - val user = senderInfo?.first?.toBestDisplayName() ?: "" - var title = applicationContext.getString(R.string.app_notification_zaps_channel_message, amount) - senderInfo?.second?.ifBlank { null }?.let { - title += " ($it)" - } - var content = applicationContext.getString(R.string.app_notification_zaps_channel_message_from, user) - zappedContent?.let { - content += " " + applicationContext.getString(R.string.app_notification_zaps_channel_message_for, zappedContent) - } - val userPicture = senderInfo?.first?.profilePicture() - val noteUri = "nostr:Notifications" - notificationManager().sendZapNotification(event.id, content, title, userPicture, noteUri, applicationContext) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt index a080eb797..226276f1c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt @@ -4,7 +4,7 @@ import android.util.Log import com.vitorpamplona.amethyst.AccountInfo import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.LocalPreferences -import com.vitorpamplona.amethyst.service.ExternalSignerUtils +import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.HttpClient import com.vitorpamplona.quartz.events.RelayAuthEvent import kotlinx.coroutines.Dispatchers @@ -16,24 +16,39 @@ import okhttp3.RequestBody.Companion.toRequestBody class RegisterAccounts( private val accounts: List ) { + private fun recursiveAuthCreation( + notificationToken: String, + remainingTos: List>, + output: MutableList, + onReady: (List) -> Unit + ) { + if (remainingTos.isEmpty()) { + onReady(output) + return + } + + val next = remainingTos.first() + + next.first.createAuthEvent(next.second, notificationToken) { + output.add(it) + recursiveAuthCreation(notificationToken, remainingTos.filter { next != it }, output, onReady) + } + } // creates proof that it controls all accounts private suspend fun signEventsToProveControlOfAccounts( accounts: List, - notificationToken: String - ): List { - return accounts.mapNotNull { + notificationToken: String, + onReady: (List) -> Unit + ) { + val readyToSend = accounts.mapNotNull { val acc = LocalPreferences.loadCurrentAccountFromEncryptedStorage(it.npub) - if (acc != null && (acc.isWriteable() || acc.loginWithExternalSigner)) { - if (acc.loginWithExternalSigner) { - ExternalSignerUtils.account = acc - } - + if (acc != null && acc.isWriteable()) { val readRelays = acc.userProfile().latestContactList?.relays() ?: acc.backupContactList?.relays() val relayToUse = readRelays?.firstNotNullOfOrNull { if (it.value.read) it.key else null } if (relayToUse != null) { - acc.createAuthEvent(relayToUse, notificationToken) + Pair(acc, relayToUse) } else { null } @@ -41,6 +56,14 @@ class RegisterAccounts( null } } + + val listOfAuthEvents = mutableListOf() + recursiveAuthCreation( + notificationToken, + readyToSend, + listOfAuthEvents, + onReady + ) } fun postRegistrationEvent(events: List) { @@ -75,9 +98,10 @@ class RegisterAccounts( } suspend fun go(notificationToken: String) = withContext(Dispatchers.IO) { - postRegistrationEvent( - signEventsToProveControlOfAccounts(accounts, notificationToken) - ) + signEventsToProveControlOfAccounts(accounts, notificationToken) { + postRegistrationEvent(it) + } + PushNotificationUtils.hasInit = true } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt index 7110b2ada..5919c1f0d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt @@ -25,7 +25,6 @@ import androidx.lifecycle.viewmodel.compose.viewModel import com.google.accompanist.adaptive.calculateDisplayFeatures import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.ServiceManager -import com.vitorpamplona.amethyst.service.ExternalSignerUtils import com.vitorpamplona.amethyst.service.lang.LanguageTranslatorService import com.vitorpamplona.amethyst.service.notifications.PushNotificationUtils import com.vitorpamplona.amethyst.ui.components.DefaultMutedSetting @@ -53,15 +52,15 @@ import java.nio.charset.StandardCharsets class MainActivity : AppCompatActivity() { private val isOnMobileDataState = mutableStateOf(false) + private val isOnWifiDataState = mutableStateOf(false) // Service Manager is only active when the activity is active. - private val serviceManager = ServiceManager() + val serviceManager = ServiceManager() + private var shouldPauseService = true @OptIn(ExperimentalMaterial3WindowSizeClassApi::class) @RequiresApi(Build.VERSION_CODES.R) override fun onCreate(savedInstanceState: Bundle?) { - ExternalSignerUtils.start(this) - super.onCreate(savedInstanceState) setContent { @@ -97,6 +96,10 @@ class MainActivity : AppCompatActivity() { } } + fun prepareToLaunchSigner() { + shouldPauseService = false + } + @OptIn(DelicateCoroutinesApi::class) override fun onResume() { super.onResume() @@ -104,8 +107,8 @@ class MainActivity : AppCompatActivity() { // starts muted every time DefaultMutedSetting.value = true - // Only starts after login - if (serviceManager.shouldPauseService) { + // Keep connection alive if it's calling the signer app + if (shouldPauseService) { GlobalScope.launch(Dispatchers.IO) { serviceManager.justStart() } @@ -116,6 +119,9 @@ class MainActivity : AppCompatActivity() { } (getSystemService(ConnectivityManager::class.java) as ConnectivityManager).registerDefaultNetworkCallback(networkCallback) + + // resets state until next External Signer Call + shouldPauseService = true } override fun onPause() { @@ -128,7 +134,7 @@ class MainActivity : AppCompatActivity() { } // } - if (serviceManager.shouldPauseService) { + if (shouldPauseService) { GlobalScope.launch(Dispatchers.IO) { serviceManager.pauseForGood() } @@ -166,7 +172,7 @@ class MainActivity : AppCompatActivity() { super.onAvailable(network) GlobalScope.launch(Dispatchers.IO) { - serviceManager.forceRestartIfItShould() + serviceManager.forceRestart() } } @@ -182,10 +188,24 @@ class MainActivity : AppCompatActivity() { val isOnWifi = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) Log.d("ServiceManager NetworkCallback", "onCapabilitiesChanged: ${network.networkHandle} hasMobileData $isOnMobileData hasWifi $isOnWifi") + var changedNetwork = false + if (isOnMobileDataState.value != isOnMobileData) { isOnMobileDataState.value = isOnMobileData - serviceManager.forceRestartIfItShould() + changedNetwork = true + } + + if (isOnWifiDataState.value != isOnWifi) { + isOnWifiDataState.value = isOnWifi + + changedNetwork = true + } + + if (changedNetwork) { + GlobalScope.launch(Dispatchers.IO) { + serviceManager.forceRestart() + } } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageUploader.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageUploader.kt index 0f400feae..b9f3f2cf2 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageUploader.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageUploader.kt @@ -10,6 +10,7 @@ import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.ServersAvailable import com.vitorpamplona.amethyst.service.HttpClient +import com.vitorpamplona.amethyst.service.checkNotInMainThread import okhttp3.Call import okhttp3.Callback import okhttp3.MediaType.Companion.toMediaType @@ -26,9 +27,7 @@ import java.util.Base64 val charPool: List = ('a'..'z') + ('A'..'Z') + ('0'..'9') fun randomChars() = List(16) { charPool.random() }.joinToString("") -object ImageUploader { - lateinit var account: Account - +class ImageUploader(val account: Account?) { fun uploadImage( uri: Uri, contentType: String?, @@ -109,61 +108,73 @@ object ImageUploader { ) .build() - server.clientID(requestBody.toString())?.let { - requestBuilder.addHeader("Authorization", it) - } + server.authorizationToken(account, requestBody.toString()) { authorizationToken -> + if (authorizationToken != null) { + requestBuilder.addHeader("Authorization", authorizationToken) + } - requestBuilder - .addHeader("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}") - .url(server.postUrl(contentType)) - .post(requestBody) - val request = requestBuilder.build() + requestBuilder + .addHeader("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}") + .url(server.postUrl(contentType)) + .post(requestBody) + val request = requestBuilder.build() - client.newCall(request).enqueue(object : Callback { - override fun onResponse(call: Call, response: Response) { - try { - check(response.isSuccessful) - response.body.use { body -> - val url = server.parseUrlFromSuccess(body.string()) - checkNotNull(url) { - "There must be an uploaded image URL in the response" + client.newCall(request).enqueue(object : Callback { + override fun onResponse(call: Call, response: Response) { + try { + check(response.isSuccessful) + response.body.use { body -> + val url = server.parseUrlFromSuccess(body.string(), authorizationToken) + checkNotNull(url) { + "There must be an uploaded image URL in the response" + } + + onSuccess(url, contentType) } - - onSuccess(url, contentType) + } catch (e: Exception) { + e.printStackTrace() + onError(e) } - } catch (e: Exception) { + } + + override fun onFailure(call: Call, e: IOException) { e.printStackTrace() onError(e) } - } - - override fun onFailure(call: Call, e: IOException) { - e.printStackTrace() - onError(e) - } - }) + }) + } } - fun NIP98Header(url: String, method: String, body: String): String { - val noteJson = account.createHTTPAuthorization(url, method, body)?.toJson() ?: "" + fun NIP98Header(url: String, method: String, body: String, onReady: (String?) -> Unit) { + val myAccount = account - val encodedNIP98Event: String = Base64.getEncoder().encodeToString(noteJson.toByteArray()) - return "Nostr " + encodedNIP98Event + if (myAccount == null) { + onReady(null) + return + } + + myAccount.createHTTPAuthorization(url, method, body) { + val noteJson = it.toJson() + val encodedNIP98Event: String = Base64.getEncoder().encodeToString(noteJson.toByteArray()) + onReady("Nostr $encodedNIP98Event") + } } } abstract class FileServer { abstract fun postUrl(contentType: String?): String - abstract fun parseUrlFromSuccess(body: String): String? + abstract fun parseUrlFromSuccess(body: String, authorizationToken: String?): String? abstract fun inputParameterName(contentType: String?): String - open fun clientID(info: String): String? = null + open fun authorizationToken(account: Account?, info: String, onReady: (String?) -> Unit) { + onReady(null) + } } class NostrImgServer : FileServer() { override fun postUrl(contentType: String?) = "https://nostrimg.com/api/upload" - override fun parseUrlFromSuccess(body: String): String? { + override fun parseUrlFromSuccess(body: String, authorizationToken: String?): String? { val tree = jacksonObjectMapper().readTree(body) val url = tree?.get("data")?.get("link")?.asText() return url @@ -172,8 +183,6 @@ class NostrImgServer : FileServer() { override fun inputParameterName(contentType: String?): String { return contentType?.toMediaType()?.toString()?.split("/")?.get(0) ?: "image" } - - override fun clientID(info: String) = null } class ImgurServer : FileServer() { @@ -182,7 +191,7 @@ class ImgurServer : FileServer() { return if (category == "image") "https://api.imgur.com/3/image" else "https://api.imgur.com/3/upload" } - override fun parseUrlFromSuccess(body: String): String? { + override fun parseUrlFromSuccess(body: String, authorizationToken: String?): String? { val tree = jacksonObjectMapper().readTree(body) val url = tree?.get("data")?.get("link")?.asText() return url @@ -192,12 +201,14 @@ class ImgurServer : FileServer() { return contentType?.toMediaType()?.toString()?.split("/")?.get(0) ?: "image" } - override fun clientID(info: String) = "Client-ID e6aea87296f3f96" + override fun authorizationToken(account: Account?, info: String, onReady: (String?) -> Unit) { + onReady("Client-ID e6aea87296f3f96") + } } class NostrBuildServer : FileServer() { override fun postUrl(contentType: String?) = "https://nostr.build/api/v2/upload/files" - override fun parseUrlFromSuccess(body: String): String? { + override fun parseUrlFromSuccess(body: String, authorizationToken: String?): String? { val tree = jacksonObjectMapper().readTree(body) val data = tree?.get("data") val data0 = data?.get(0) @@ -209,12 +220,14 @@ class NostrBuildServer : FileServer() { return "file" } - override fun clientID(info: String) = ImageUploader.NIP98Header("https://nostr.build/api/v2/upload/files", "POST", info) + override fun authorizationToken(account: Account?, info: String, onReady: (String?) -> Unit) { + ImageUploader(account).NIP98Header("https://nostr.build/api/v2/upload/files", "POST", info, onReady) + } } class NostrFilesDevServer : FileServer() { override fun postUrl(contentType: String?) = "https://nostrfiles.dev/upload_image" - override fun parseUrlFromSuccess(body: String): String? { + override fun parseUrlFromSuccess(body: String, authorizationToken: String?): String? { val tree = jacksonObjectMapper().readTree(body) return tree?.get("url")?.asText() } @@ -222,26 +235,29 @@ class NostrFilesDevServer : FileServer() { override fun inputParameterName(contentType: String?): String { return "file" } - - override fun clientID(info: String) = null } class NostrCheckMeServer : FileServer() { override fun postUrl(contentType: String?) = "https://nostrcheck.me/api/v1/media" - override fun parseUrlFromSuccess(body: String): String? { + override fun parseUrlFromSuccess(body: String, authorizationToken: String?): String? { + checkNotInMainThread() + val tree = jacksonObjectMapper().readTree(body) val url = tree?.get("url")?.asText() - var id = tree?.get("id")?.asText() + val id = tree?.get("id")?.asText() var isCompleted = false val client = HttpClient.getHttpClient() - var requrl = "https://nostrcheck.me/api/v1/media?id=" + id // + "&apikey=26d075787d261660682fb9d20dbffa538c708b1eda921d0efa2be95fbef4910a" + val requrl = + "https://nostrcheck.me/api/v1/media?id=$id" // + "&apikey=26d075787d261660682fb9d20dbffa538c708b1eda921d0efa2be95fbef4910a" - val request = Request.Builder() - .url(requrl) - .addHeader("Authorization", ImageUploader.NIP98Header(requrl, "GET", "")) - .get() - .build() + val requestBuilder = Request.Builder().url(requrl) + + if (authorizationToken != null) { + requestBuilder.addHeader("Authorization", authorizationToken) + } + + val request = requestBuilder.get().build() while (!isCompleted) { client.newCall(request).execute().use { @@ -261,5 +277,7 @@ class NostrCheckMeServer : FileServer() { return "mediafile" } - override fun clientID(body: String) = ImageUploader.NIP98Header("https://nostrcheck.me/api/v1/media", "POST", body) + override fun authorizationToken(account: Account?, body: String, onReady: (String?) -> Unit) { + ImageUploader(account).NIP98Header("https://nostrcheck.me/api/v1/media", "POST", body, onReady) + } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt index cdc8f2e88..7affff81e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt @@ -95,7 +95,7 @@ open class NewMediaModel : ViewModel() { uploadingPercentage.value = 0.2f uploadingDescription.value = "Uploading" viewModelScope.launch(Dispatchers.IO) { - ImageUploader.uploadImage( + ImageUploader(account).uploadImage( uri = fileUri, contentType = contentType, size = size, @@ -173,11 +173,12 @@ open class NewMediaModel : ViewModel() { onReady = { uploadingPercentage.value = 0.90f uploadingDescription.value = "Sending" - account?.sendHeader(it, relayList) - uploadingPercentage.value = 1.00f - isUploadingImage = false - onceUploaded() - cancel() + account?.sendHeader(it, relayList) { + uploadingPercentage.value = 1.00f + isUploadingImage = false + onceUploaded() + cancel() + } }, onError = { cancel() @@ -216,18 +217,16 @@ open class NewMediaModel : ViewModel() { onReady = { uploadingDescription.value = "Signing" uploadingPercentage.value = 0.40f - val nip95 = account?.createNip95(bytes, headerInfo = it) - - if (nip95 != null) { + account?.createNip95(bytes, headerInfo = it) { nip95 -> uploadingDescription.value = "Sending" uploadingPercentage.value = 0.60f account?.sendNip95(nip95.first, nip95.second, relayList) - } - uploadingPercentage.value = 1.00f - isUploadingImage = false - onceUploaded() - cancel() + uploadingPercentage.value = 1.00f + isUploadingImage = false + onceUploaded() + cancel() + } }, onError = { uploadingDescription.value = null diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt index b64d762c6..ca6856d7c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt @@ -199,8 +199,11 @@ fun NewPostView( } DisposableEffect(Unit) { + NostrSearchEventOrUserDataSource.start() + onDispose { NostrSearchEventOrUserDataSource.clear() + NostrSearchEventOrUserDataSource.stop() } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt index ba0075961..ca57120b2 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt @@ -124,6 +124,9 @@ open class NewPostViewModel() : ViewModel() { var nip24 by mutableStateOf(false) open fun load(accountViewModel: AccountViewModel, replyingTo: Note?, quote: Note?) { + this.accountViewModel = accountViewModel + this.account = accountViewModel.account + originalNote = replyingTo replyingTo?.let { replyNote -> if (replyNote.event is BaseTextNoteEvent) { @@ -167,9 +170,6 @@ open class NewPostViewModel() : ViewModel() { zapRaiserAmount = null forwardZapTo = Split() forwardZapToEditting = TextFieldValue("") - - this.accountViewModel = accountViewModel - this.account = accountViewModel.account } fun sendPost(relayList: List? = null) { @@ -326,7 +326,7 @@ open class NewPostViewModel() : ViewModel() { } } else { viewModelScope.launch(Dispatchers.IO) { - ImageUploader.uploadImage( + ImageUploader(account).uploadImage( uri = fileUri, contentType = contentType, size = size, @@ -556,17 +556,13 @@ open class NewPostViewModel() : ViewModel() { alt, sensitiveContent, onReady = { - val note = account?.sendHeader(it, relayList = relayList) + account?.sendHeader(it, relayList = relayList) { note -> + isUploadingImage = false - isUploadingImage = false - - if (note == null) { - message = TextFieldValue(message.text + "\n" + imageUrl) - } else { message = TextFieldValue(message.text + "\nnostr:" + note.toNEvent()) - } - urlPreview = findUrlInMessage() + urlPreview = findUrlInMessage() + } }, onError = { isUploadingImage = false @@ -587,16 +583,17 @@ open class NewPostViewModel() : ViewModel() { alt, sensitiveContent, onReady = { - val nip95 = account?.createNip95(bytes, headerInfo = it) - val note = nip95?.let { it1 -> account?.sendNip95(it1.first, it1.second, relayList = relayList) } + account?.createNip95(bytes, headerInfo = it) { nip95 -> + val note = nip95.let { it1 -> account?.sendNip95(it1.first, it1.second, relayList = relayList) } - isUploadingImage = false + isUploadingImage = false - note?.let { - message = TextFieldValue(message.text + "\nnostr:" + it.toNEvent()) + note?.let { + message = TextFieldValue(message.text + "\nnostr:" + it.toNEvent()) + } + + urlPreview = findUrlInMessage() } - - urlPreview = findUrlInMessage() }, onError = { isUploadingImage = false diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt index 5736190e0..c6fc4de3f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt @@ -184,7 +184,7 @@ class NewUserMetadataViewModel : ViewModel() { context.applicationContext, onReady = { fileUri, contentType, size -> viewModelScope.launch(Dispatchers.IO) { - ImageUploader.uploadImage( + ImageUploader(account).uploadImage( uri = fileUri, contentType = contentType, size = size, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoiceRequest.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoiceRequest.kt index 801f495a0..9980a01c0 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoiceRequest.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoiceRequest.kt @@ -38,6 +38,7 @@ import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver import com.vitorpamplona.amethyst.ui.theme.QuoteBorder import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.subtleBorder +import com.vitorpamplona.quartz.events.LnZapEvent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -155,19 +156,33 @@ fun InvoiceRequest( modifier = Modifier.fillMaxWidth().padding(vertical = 10.dp), onClick = { scope.launch(Dispatchers.IO) { - val zapRequest = account.createZapRequestFor(toUserPubKeyHex, message, account.defaultZapType) - - LightningAddressResolver().lnAddressInvoice( - lud16, - amount * 1000, - message, - zapRequest?.toJson(), - onSuccess = onSuccess, - onError = onError, - onProgress = { - }, - context = context - ) + if (account.defaultZapType == LnZapEvent.ZapType.NONZAP) { + LightningAddressResolver().lnAddressInvoice( + lud16, + amount * 1000, + message, + null, + onSuccess = onSuccess, + onError = onError, + onProgress = { + }, + context = context + ) + } else { + account.createZapRequestFor(toUserPubKeyHex, message, account.defaultZapType) { zapRequest -> + LightningAddressResolver().lnAddressInvoice( + lud16, + amount * 1000, + message, + zapRequest.toJson(), + onSuccess = onSuccess, + onError = onError, + onProgress = { + }, + context = context + ) + } + } } }, shape = QuoteBorder, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/BookmarkPrivateFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/BookmarkPrivateFeedFilter.kt index 5abbb7041..d191d8200 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/BookmarkPrivateFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/BookmarkPrivateFeedFilter.kt @@ -3,12 +3,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.amethyst.service.ExternalSignerUtils -import com.vitorpamplona.quartz.encoders.toHexKey - -object BookmarkPrivateFeedFilter : FeedFilter() { - lateinit var account: Account +class BookmarkPrivateFeedFilter(val account: Account) : FeedFilter() { override fun feedKey(): String { return account.userProfile().latestBookmarkList?.id ?: "" } @@ -16,42 +12,18 @@ object BookmarkPrivateFeedFilter : FeedFilter() { override fun feed(): List { val bookmarks = account.userProfile().latestBookmarkList - if (account.loginWithExternalSigner) { - val id = bookmarks?.id - if (id != null) { - val decryptedContent = ExternalSignerUtils.cachedDecryptedContent[id] - if (decryptedContent == null) { - ExternalSignerUtils.decryptBookmark( - bookmarks.content, - account.keyPair.pubKey.toHexKey(), - id - ) - } else { - bookmarks.decryptedContent = decryptedContent - } - } - val decryptedContent = ExternalSignerUtils.cachedDecryptedContent[id] ?: "" + if (!account.isWriteable()) return emptyList() - val notes = bookmarks?.privateTaggedEvents(decryptedContent) - ?.mapNotNull { LocalCache.checkGetOrCreateNote(it) } ?: emptyList() - val addresses = bookmarks?.privateTaggedAddresses(decryptedContent) - ?.map { LocalCache.getOrCreateAddressableNote(it) } ?: emptyList() + val privateTags = bookmarks?.cachedPrivateTags() ?: return emptyList() - return notes.plus(addresses).toSet() - .sortedWith(compareBy({ it.createdAt() }, { it.idHex })) - .reversed() - } else { - val privKey = account.keyPair.privKey ?: return emptyList() + val notes = bookmarks.filterEvents(privateTags) + .mapNotNull { LocalCache.checkGetOrCreateNote(it) } - val notes = bookmarks?.privateTaggedEvents(privKey) - ?.mapNotNull { LocalCache.checkGetOrCreateNote(it) } ?: emptyList() + val addresses = bookmarks.filterAddresses(privateTags) + .map { LocalCache.getOrCreateAddressableNote(it) } - val addresses = bookmarks?.privateTaggedAddresses(privKey) - ?.map { LocalCache.getOrCreateAddressableNote(it) } ?: emptyList() - - return notes.plus(addresses).toSet() - .sortedWith(compareBy({ it.createdAt() }, { it.idHex })) - .reversed() - } + return notes.plus(addresses).toSet() + .sortedWith(compareBy({ it.createdAt() }, { it.idHex })) + .reversed() } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/BookmarkPublicFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/BookmarkPublicFeedFilter.kt index 775e48bdc..13ee962ca 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/BookmarkPublicFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/BookmarkPublicFeedFilter.kt @@ -4,11 +4,9 @@ import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note -object BookmarkPublicFeedFilter : FeedFilter() { - lateinit var account: Account - +class BookmarkPublicFeedFilter(val account: Account) : FeedFilter() { override fun feedKey(): String { - return BookmarkPrivateFeedFilter.account.userProfile().latestBookmarkList?.id ?: "" + return account.userProfile().latestBookmarkList?.id ?: "" } override fun feed(): List { val bookmarks = account.userProfile().latestBookmarkList diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverChatFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverChatFeedFilter.kt index e313b3315..3236c5350 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverChatFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverChatFeedFilter.kt @@ -12,11 +12,11 @@ import com.vitorpamplona.quartz.utils.TimeUtils open class DiscoverChatFeedFilter(val account: Account) : AdditiveFeedFilter() { override fun feedKey(): String { - return account.userProfile().pubkeyHex + "-" + account.defaultDiscoveryFollowList + return account.userProfile().pubkeyHex + "-" + account.defaultDiscoveryFollowList.value } override fun showHiddenKey(): Boolean { - return account.defaultDiscoveryFollowList == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) + return account.defaultDiscoveryFollowList.value == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) } override fun feed(): List { @@ -33,12 +33,12 @@ open class DiscoverChatFeedFilter(val account: Account) : AdditiveFeedFilter): Set { val now = TimeUtils.now() - val isGlobal = account.defaultDiscoveryFollowList == GLOBAL_FOLLOWS + val isGlobal = account.defaultDiscoveryFollowList.value == GLOBAL_FOLLOWS val isHiddenList = showHiddenKey() - val followingKeySet = account.selectedUsersFollowList(account.defaultDiscoveryFollowList) ?: emptySet() - val followingTagSet = account.selectedTagsFollowList(account.defaultDiscoveryFollowList) ?: emptySet() - val followingGeohashSet = account.selectedGeohashesFollowList(account.defaultDiscoveryFollowList) ?: emptySet() + val followingKeySet = account.liveDiscoveryFollowLists.value?.users ?: emptySet() + val followingTagSet = account.liveDiscoveryFollowLists.value?.hashtags ?: emptySet() + val followingGeohashSet = account.liveDiscoveryFollowLists.value?.geotags ?: emptySet() val createEvents = collection.filter { it.event is ChannelCreateEvent } val anyOtherChannelEvent = collection @@ -60,7 +60,7 @@ open class DiscoverChatFeedFilter(val account: Account) : AdditiveFeedFilter): List { - val followingKeySet = account.selectedUsersFollowList(account.defaultDiscoveryFollowList) + val followingKeySet = account.liveDiscoveryFollowLists.value?.users ?: account.liveKind3Follows.value.users val counter = ParticipantListBuilder() val participantCounts = collection.associate { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverCommunityFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverCommunityFeedFilter.kt index 0a1faa2c6..0fbd8ef8f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverCommunityFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverCommunityFeedFilter.kt @@ -12,11 +12,11 @@ import com.vitorpamplona.quartz.utils.TimeUtils open class DiscoverCommunityFeedFilter(val account: Account) : AdditiveFeedFilter() { override fun feedKey(): String { - return account.userProfile().pubkeyHex + "-" + account.defaultDiscoveryFollowList + return account.userProfile().pubkeyHex + "-" + account.defaultDiscoveryFollowList.value } override fun showHiddenKey(): Boolean { - return account.defaultDiscoveryFollowList == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) + return account.defaultDiscoveryFollowList.value == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) } override fun feed(): List { @@ -33,12 +33,12 @@ open class DiscoverCommunityFeedFilter(val account: Account) : AdditiveFeedFilte protected open fun innerApplyFilter(collection: Collection): Set { val now = TimeUtils.now() - val isGlobal = account.defaultDiscoveryFollowList == GLOBAL_FOLLOWS + val isGlobal = account.defaultDiscoveryFollowList.value == GLOBAL_FOLLOWS val isHiddenList = showHiddenKey() - val followingKeySet = account.selectedUsersFollowList(account.defaultDiscoveryFollowList) ?: emptySet() - val followingTagSet = account.selectedTagsFollowList(account.defaultDiscoveryFollowList) ?: emptySet() - val followingGeohashSet = account.selectedGeohashesFollowList(account.defaultDiscoveryFollowList) ?: emptySet() + val followingKeySet = account.liveDiscoveryFollowLists.value?.users ?: emptySet() + val followingTagSet = account.liveDiscoveryFollowLists.value?.hashtags ?: emptySet() + val followingGeohashSet = account.liveDiscoveryFollowLists.value?.geotags ?: emptySet() val createEvents = collection.filter { it.event is CommunityDefinitionEvent } val anyOtherCommunityEvent = collection @@ -60,16 +60,21 @@ open class DiscoverCommunityFeedFilter(val account: Account) : AdditiveFeedFilte } override fun sort(collection: Set): List { - val followingKeySet = account.selectedUsersFollowList(account.defaultDiscoveryFollowList) + val followingKeySet = account.liveDiscoveryFollowLists.value?.users ?: account.liveKind3Follows.value.users val counter = ParticipantListBuilder() val participantCounts = collection.associate { it to counter.countFollowsThatParticipateOn(it, followingKeySet) } + val allParticipants = collection.associate { + it to counter.countFollowsThatParticipateOn(it, null) + } + return collection.sortedWith( compareBy( { participantCounts[it] }, + { allParticipants[it] }, { it.createdAt() }, { it.idHex } ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverLiveFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverLiveFeedFilter.kt index 70aa99d43..71c11871b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverLiveFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverLiveFeedFilter.kt @@ -20,7 +20,7 @@ open class DiscoverLiveFeedFilter( } open fun followList(): String { - return account.defaultDiscoveryFollowList + return account.defaultDiscoveryFollowList.value } override fun showHiddenKey(): Boolean { @@ -43,12 +43,12 @@ open class DiscoverLiveFeedFilter( protected open fun innerApplyFilter(collection: Collection): Set { val now = TimeUtils.now() - val isGlobal = account.defaultDiscoveryFollowList == GLOBAL_FOLLOWS + val isGlobal = account.defaultDiscoveryFollowList.value == GLOBAL_FOLLOWS val isHiddenList = showHiddenKey() - val followingKeySet = account.selectedUsersFollowList(followList()) ?: emptySet() - val followingTagSet = account.selectedTagsFollowList(followList()) ?: emptySet() - val followingGeohashSet = account.selectedGeohashesFollowList(followList()) ?: emptySet() + val followingKeySet = account.liveDiscoveryFollowLists.value?.users ?: emptySet() + val followingTagSet = account.liveDiscoveryFollowLists.value?.hashtags ?: emptySet() + val followingGeohashSet = account.liveDiscoveryFollowLists.value?.geotags ?: emptySet() val activities = collection .asSequence() @@ -68,17 +68,22 @@ open class DiscoverLiveFeedFilter( } override fun sort(collection: Set): List { - val followingKeySet = account.selectedUsersFollowList(followList()) + val followingKeySet = account.liveDiscoveryFollowLists.value?.users ?: account.liveKind3Follows.value.users val counter = ParticipantListBuilder() val participantCounts = collection.associate { it to counter.countFollowsThatParticipateOn(it, followingKeySet) } + val allParticipants = collection.associate { + it to counter.countFollowsThatParticipateOn(it, null) + } + return collection.sortedWith( compareBy( { convertStatusToOrder((it.event as? LiveActivitiesEvent)?.status()) }, { participantCounts[it] }, + { allParticipants[it] }, { (it.event as? LiveActivitiesEvent)?.starts() ?: it.createdAt() }, { it.idHex } ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HiddenAccountsFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HiddenAccountsFeedFilter.kt index a3f967ff4..06a7ead3e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HiddenAccountsFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HiddenAccountsFeedFilter.kt @@ -14,21 +14,9 @@ class HiddenAccountsFeedFilter(val account: Account) : FeedFilter() { } override fun feed(): List { - val blockList = account.getBlockList() - val decryptedContent = blockList?.decryptedContent ?: "" - if (account.loginWithExternalSigner) { - if (decryptedContent.isEmpty()) return emptyList() - - return blockList - ?.publicAndPrivateUsers(decryptedContent) - ?.map { LocalCache.getOrCreateUser(it) } - ?: emptyList() - } - - return blockList - ?.publicAndPrivateUsers(account.keyPair.privKey) - ?.map { LocalCache.getOrCreateUser(it) } - ?: emptyList() + return account.liveHiddenUsers.value?.hiddenUsers?.map { + LocalCache.getOrCreateUser(it) + } ?: emptyList() } } @@ -42,19 +30,7 @@ class HiddenWordsFeedFilter(val account: Account) : FeedFilter() { } override fun feed(): List { - val blockList = account.getBlockList() - val decryptedContent = blockList?.decryptedContent ?: "" - if (account.loginWithExternalSigner) { - if (decryptedContent.isEmpty()) return emptyList() - - return blockList - ?.publicAndPrivateWords(decryptedContent)?.toList() - ?: emptyList() - } - - return blockList - ?.publicAndPrivateWords(account.keyPair.privKey)?.toList() - ?: emptyList() + return account.liveHiddenUsers.value?.hiddenWords?.toList() ?: emptyList() } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeConversationsFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeConversationsFeedFilter.kt index c8e7d481c..350c4224d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeConversationsFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeConversationsFeedFilter.kt @@ -14,11 +14,11 @@ import com.vitorpamplona.quartz.utils.TimeUtils class HomeConversationsFeedFilter(val account: Account) : AdditiveFeedFilter() { override fun feedKey(): String { - return account.userProfile().pubkeyHex + "-" + account.defaultHomeFollowList + return account.userProfile().pubkeyHex + "-" + account.defaultHomeFollowList.value } override fun showHiddenKey(): Boolean { - return account.defaultHomeFollowList == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) + return account.defaultHomeFollowList.value == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) } override fun feed(): List { @@ -30,12 +30,12 @@ class HomeConversationsFeedFilter(val account: Account) : AdditiveFeedFilter): Set { - val isGlobal = account.defaultHomeFollowList == GLOBAL_FOLLOWS + val isGlobal = account.defaultHomeFollowList.value == GLOBAL_FOLLOWS val isHiddenList = showHiddenKey() - val followingKeySet = account.selectedUsersFollowList(account.defaultHomeFollowList) ?: emptySet() - val followingTagSet = account.selectedTagsFollowList(account.defaultHomeFollowList) ?: emptySet() - val followingGeoHashSet = account.selectedGeohashesFollowList(account.defaultHomeFollowList) ?: emptySet() + val followingKeySet = account.liveHomeFollowLists.value?.users ?: emptySet() + val followingTagSet = account.liveHomeFollowLists.value?.hashtags ?: emptySet() + val followingGeohashSet = account.liveHomeFollowLists.value?.geotags ?: emptySet() val now = TimeUtils.now() @@ -43,7 +43,7 @@ class HomeConversationsFeedFilter(val account: Account) : AdditiveFeedFilter() { override fun feedKey(): String { - return account.userProfile().pubkeyHex + "-" + account.defaultHomeFollowList + return account.userProfile().pubkeyHex + "-" + account.defaultHomeFollowList.value } override fun showHiddenKey(): Boolean { - return account.defaultHomeFollowList == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) + return account.defaultHomeFollowList.value == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) } override fun feed(): List { @@ -38,14 +38,14 @@ class HomeNewThreadFeedFilter(val account: Account) : AdditiveFeedFilter() } private fun innerApplyFilter(collection: Collection, ignoreAddressables: Boolean): Set { - val isGlobal = account.defaultHomeFollowList == GLOBAL_FOLLOWS + val isGlobal = account.defaultHomeFollowList.value == GLOBAL_FOLLOWS val gRelays = account.activeGlobalRelays() val isHiddenList = showHiddenKey() - val followingKeySet = account.selectedUsersFollowList(account.defaultHomeFollowList) ?: emptySet() - val followingTagSet = account.selectedTagsFollowList(account.defaultHomeFollowList) ?: emptySet() - val followingGeoSet = account.selectedGeohashesFollowList(account.defaultHomeFollowList) ?: emptySet() - val followingCommunities = account.selectedCommunitiesFollowList(account.defaultHomeFollowList) ?: emptySet() + val followingKeySet = account.liveHomeFollowLists.value?.users ?: emptySet() + val followingTagSet = account.liveHomeFollowLists.value?.hashtags ?: emptySet() + val followingGeohashSet = account.liveHomeFollowLists.value?.geotags ?: emptySet() + val followingCommunities = account.liveHomeFollowLists.value?.communities ?: emptySet() val oneMinuteInTheFuture = TimeUtils.now() + (1 * 60) // one minute in the future. val oneHr = 60 * 60 @@ -57,7 +57,7 @@ class HomeNewThreadFeedFilter(val account: Account) : AdditiveFeedFilter() val isGlobalRelay = it.relays.any { gRelays.contains(it) } (noteEvent is TextNoteEvent || noteEvent is ClassifiedsEvent || noteEvent is RepostEvent || noteEvent is GenericRepostEvent || noteEvent is LongTextNoteEvent || noteEvent is PollNoteEvent || noteEvent is HighlightEvent || noteEvent is AudioTrackEvent || noteEvent is AudioHeaderEvent) && (!ignoreAddressables || noteEvent.kind() < 10000) && - ((isGlobal && isGlobalRelay) || it.author?.pubkeyHex in followingKeySet || noteEvent.isTaggedHashes(followingTagSet) || noteEvent.isTaggedGeoHashes(followingGeoSet) || noteEvent.isTaggedAddressableNotes(followingCommunities)) && + ((isGlobal && isGlobalRelay) || it.author?.pubkeyHex in followingKeySet || noteEvent.isTaggedHashes(followingTagSet) || noteEvent.isTaggedGeoHashes(followingGeohashSet) || noteEvent.isTaggedAddressableNotes(followingCommunities)) && // && account.isAcceptable(it) // This filter follows only. No need to check if acceptable (isHiddenList || it.author?.let { !account.isHidden(it.pubkeyHex) } ?: true) && ((it.event?.createdAt() ?: 0) < oneMinuteInTheFuture) && diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt index d1927b8c6..5031128c5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt @@ -20,11 +20,11 @@ import com.vitorpamplona.quartz.events.RepostEvent class NotificationFeedFilter(val account: Account) : AdditiveFeedFilter() { override fun feedKey(): String { - return account.userProfile().pubkeyHex + "-" + account.defaultNotificationFollowList + return account.userProfile().pubkeyHex + "-" + account.defaultNotificationFollowList.value } override fun showHiddenKey(): Boolean { - return account.defaultNotificationFollowList == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) + return account.defaultNotificationFollowList.value == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) } override fun feed(): List { @@ -36,10 +36,10 @@ class NotificationFeedFilter(val account: Account) : AdditiveFeedFilter() } private fun innerApplyFilter(collection: Collection): Set { - val isGlobal = account.defaultNotificationFollowList == GLOBAL_FOLLOWS + val isGlobal = account.defaultNotificationFollowList.value == GLOBAL_FOLLOWS val isHiddenList = showHiddenKey() - val followingKeySet = account.selectedUsersFollowList(account.defaultNotificationFollowList) ?: emptySet() + val followingKeySet = account.liveNotificationFollowLists.value?.users ?: emptySet() val loggedInUser = account.userProfile() val loggedInUserHex = loggedInUser.pubkeyHex diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ThreadFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ThreadFeedFilter.kt index a216db8a4..615f20dd1 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ThreadFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ThreadFeedFilter.kt @@ -2,7 +2,6 @@ package com.vitorpamplona.amethyst.ui.dal import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.KIND3_FOLLOWS import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.ThreadAssembler import com.vitorpamplona.quartz.utils.TimeUtils @@ -16,14 +15,14 @@ class ThreadFeedFilter(val account: Account, val noteId: String) : FeedFilter { val cachedSignatures: MutableMap = mutableMapOf() - val followingSet = account.selectedUsersFollowList(KIND3_FOLLOWS) ?: emptySet() + val followingKeySet = account.liveKind3Follows.value.users val eventsToWatch = ThreadAssembler().findThreadFor(noteId) val eventsInHex = eventsToWatch.map { it.idHex }.toSet() val now = TimeUtils.now() // Currently orders by date of each event, descending, at each level of the reply stack val order = compareByDescending { - it.replyLevelSignature(eventsInHex, cachedSignatures, account.userProfile(), followingSet, now).signature + it.replyLevelSignature(eventsInHex, cachedSignatures, account.userProfile(), followingKeySet, now).signature } return eventsToWatch.sortedWith(order) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/VideoFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/VideoFeedFilter.kt index b79f32369..b3e5d3c28 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/VideoFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/VideoFeedFilter.kt @@ -11,11 +11,11 @@ import com.vitorpamplona.quartz.utils.TimeUtils class VideoFeedFilter(val account: Account) : AdditiveFeedFilter() { override fun feedKey(): String { - return account.userProfile().pubkeyHex + "-" + account.defaultStoriesFollowList + return account.userProfile().pubkeyHex + "-" + account.defaultStoriesFollowList.value } override fun showHiddenKey(): Boolean { - return account.defaultStoriesFollowList == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) + return account.defaultStoriesFollowList.value == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) } override fun feed(): List { @@ -30,12 +30,12 @@ class VideoFeedFilter(val account: Account) : AdditiveFeedFilter() { private fun innerApplyFilter(collection: Collection): Set { val now = TimeUtils.now() - val isGlobal = account.defaultStoriesFollowList == GLOBAL_FOLLOWS - val isHiddenList = account.defaultStoriesFollowList == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) + val isGlobal = account.defaultStoriesFollowList.value == GLOBAL_FOLLOWS + val isHiddenList = account.defaultStoriesFollowList.value == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) - val followingKeySet = account.selectedUsersFollowList(account.defaultStoriesFollowList) ?: emptySet() - val followingTagSet = account.selectedTagsFollowList(account.defaultStoriesFollowList) ?: emptySet() - val followingGeohashSet = account.selectedGeohashesFollowList(account.defaultStoriesFollowList) ?: emptySet() + val followingKeySet = account.liveStoriesFollowLists.value?.users ?: emptySet() + val followingTagSet = account.liveStoriesFollowLists.value?.hashtags ?: emptySet() + val followingGeohashSet = account.liveStoriesFollowLists.value?.geotags ?: emptySet() return collection .asSequence() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt index 332d1934d..877df2534 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt @@ -373,7 +373,7 @@ fun NoTopBar() { @Composable fun StoriesTopBar(followLists: FollowListViewModel, drawerState: DrawerState, accountViewModel: AccountViewModel, nav: (String) -> Unit) { GenericMainTopBar(drawerState, accountViewModel, nav) { accountViewModel -> - val list by accountViewModel.storiesListLiveData.observeAsState(GLOBAL_FOLLOWS) + val list by accountViewModel.account.defaultStoriesFollowList.collectAsStateWithLifecycle() FollowListWithRoutes( followListsModel = followLists, @@ -387,7 +387,7 @@ fun StoriesTopBar(followLists: FollowListViewModel, drawerState: DrawerState, ac @Composable fun HomeTopBar(followLists: FollowListViewModel, drawerState: DrawerState, accountViewModel: AccountViewModel, nav: (String) -> Unit) { GenericMainTopBar(drawerState, accountViewModel, nav) { accountViewModel -> - val list by accountViewModel.homeListLiveData.observeAsState(KIND3_FOLLOWS) + val list by accountViewModel.account.defaultHomeFollowList.collectAsStateWithLifecycle() FollowListWithRoutes( followListsModel = followLists, @@ -405,7 +405,7 @@ fun HomeTopBar(followLists: FollowListViewModel, drawerState: DrawerState, accou @Composable fun NotificationTopBar(followLists: FollowListViewModel, drawerState: DrawerState, accountViewModel: AccountViewModel, nav: (String) -> Unit) { GenericMainTopBar(drawerState, accountViewModel, nav) { accountViewModel -> - val list by accountViewModel.notificationListLiveData.observeAsState(GLOBAL_FOLLOWS) + val list by accountViewModel.account.defaultNotificationFollowList.collectAsStateWithLifecycle() FollowListWithoutRoutes( followListsModel = followLists, @@ -419,7 +419,7 @@ fun NotificationTopBar(followLists: FollowListViewModel, drawerState: DrawerStat @Composable fun DiscoveryTopBar(followLists: FollowListViewModel, drawerState: DrawerState, accountViewModel: AccountViewModel, nav: (String) -> Unit) { GenericMainTopBar(drawerState, accountViewModel, nav) { accountViewModel -> - val list by accountViewModel.discoveryListLiveData.observeAsState(GLOBAL_FOLLOWS) + val list by accountViewModel.account.defaultDiscoveryFollowList.collectAsStateWithLifecycle() FollowListWithoutRoutes( followListsModel = followLists, @@ -693,7 +693,7 @@ fun SimpleTextSpinner( id = R.string.select_an_option ) - var currentText by remember(placeholderCode) { + var currentText by remember(placeholderCode, options) { mutableStateOf( options.firstOrNull { it.code == placeholderCode }?.name?.name(context) ?: selectAnOption ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt index 9c9907667..87e1d2f4e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt @@ -36,7 +36,6 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.MutableState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState @@ -62,12 +61,8 @@ import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil.compose.AsyncImage import com.vitorpamplona.amethyst.BuildConfig -import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.ServiceManager -import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.HttpClient import com.vitorpamplona.amethyst.service.relays.RelayPool import com.vitorpamplona.amethyst.service.relays.RelayPoolStatus import com.vitorpamplona.amethyst.ui.actions.NewRelayListView @@ -564,9 +559,7 @@ fun ListContent( conectOrbotDialogOpen = false disconnectTorDialog = false checked = true - coroutineScope.launch(Dispatchers.IO) { - enableTor(accountViewModel.account, true, proxyPort) - } + accountViewModel.enableTor(true, proxyPort) }, onError = { accountViewModel.toast( @@ -594,13 +587,7 @@ fun ListContent( onClick = { disconnectTorDialog = false checked = false - coroutineScope.launch(Dispatchers.IO) { - enableTor( - accountViewModel.account, - false, - proxyPort - ) - } + accountViewModel.enableTor(false, proxyPort) } ) { Text(text = stringResource(R.string.yes)) @@ -619,17 +606,6 @@ fun ListContent( } } -private suspend fun enableTor( - account: Account, - checked: Boolean, - portNumber: MutableState -) { - account.proxyPort = portNumber.value.toInt() - account.proxy = HttpClient.initProxy(checked, "127.0.0.1", account.proxyPort) - LocalPreferences.saveToEncryptedStorage(account) - ServiceManager.forceRestart() -} - @Composable private fun RelayStatus(accountViewModel: AccountViewModel) { val connectedRelaysText by RelayPool.statusFlow.collectAsStateWithLifecycle(RelayPoolStatus(0, 0)) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt index 91852b57a..2bd95649a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt @@ -45,7 +45,6 @@ import androidx.lifecycle.distinctUntilChanged import androidx.lifecycle.map import coil.compose.AsyncImage import com.vitorpamplona.amethyst.model.Channel -import com.vitorpamplona.amethyst.model.KIND3_FOLLOWS import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.ParticipantListBuilder @@ -626,11 +625,11 @@ fun LoadModerators( } } - val followingKeySet = accountViewModel.account.selectedUsersFollowList(accountViewModel.account.defaultDiscoveryFollowList) + val followingKeySet = accountViewModel.account.liveDiscoveryFollowLists.value?.users val allParticipants = ParticipantListBuilder().followsThatParticipateOn(baseNote, followingKeySet).minus(hosts) val newParticipantUsers = if (followingKeySet == null) { - val allFollows = accountViewModel.account.selectedUsersFollowList(KIND3_FOLLOWS) + val allFollows = accountViewModel.account.userProfile().cachedFollowingKeySet() val followingParticipants = ParticipantListBuilder().followsThatParticipateOn(baseNote, allFollows).minus(hosts) (hosts + followingParticipants + (allParticipants - followingParticipants)).toImmutableList() @@ -676,11 +675,12 @@ private fun LoadParticipants( } ?: emptyList() ) - val followingKeySet = accountViewModel.account.selectedUsersFollowList(accountViewModel.account.defaultDiscoveryFollowList) + val followingKeySet = accountViewModel.account.liveDiscoveryFollowLists.value?.users + val allParticipants = ParticipantListBuilder().followsThatParticipateOn(baseNote, followingKeySet).minus(hostsAuthor) val newParticipantUsers = if (followingKeySet == null) { - val allFollows = accountViewModel.account.selectedUsersFollowList(KIND3_FOLLOWS) + val allFollows = accountViewModel.account.userProfile().cachedFollowingKeySet() val followingParticipants = ParticipantListBuilder().followsThatParticipateOn(baseNote, allFollows).minus(hostsAuthor) (hosts + followingParticipants + (allParticipants - followingParticipants)).toImmutableList() @@ -726,11 +726,11 @@ fun RenderChannelThumb(baseNote: Note, channel: Channel, accountViewModel: Accou LaunchedEffect(key1 = channelUpdates) { launch(Dispatchers.IO) { - val followingKeySet = accountViewModel.account.selectedUsersFollowList(accountViewModel.account.defaultDiscoveryFollowList) + val followingKeySet = accountViewModel.account.liveDiscoveryFollowLists.value?.users val allParticipants = ParticipantListBuilder().followsThatParticipateOn(baseNote, followingKeySet).toImmutableList() val newParticipantUsers = if (followingKeySet == null) { - val allFollows = accountViewModel.account.selectedUsersFollowList(KIND3_FOLLOWS) + val allFollows = accountViewModel.account.userProfile().cachedFollowingKeySet() val followingParticipants = ParticipantListBuilder().followsThatParticipateOn(baseNote, allFollows).toList() (followingParticipants + (allParticipants - followingParticipants)).toImmutableList() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomHeaderCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomHeaderCompose.kt index 1fb30335f..b987a33df 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomHeaderCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomHeaderCompose.kt @@ -262,11 +262,6 @@ private fun UserRoomCompose( note.createdAt() } } - val content by remember(note) { - mutableStateOf( - accountViewModel.decrypt(note) - ) - } WatchNotificationChanges(note, route, accountViewModel) { newHasNewMessages -> if (hasNewMessages.value != newHasNewMessages) { @@ -274,22 +269,24 @@ private fun UserRoomCompose( } } - ChannelName( - channelPicture = { - NonClickableUserPictures( - users = room.users, - accountViewModel = accountViewModel, - size = Size55dp - ) - }, - channelTitle = { - RoomNameDisplay(room, it, accountViewModel) - }, - channelLastTime = createAt, - channelLastContent = content, - hasNewMessages = hasNewMessages, - onClick = { nav(route) } - ) + LoadDecryptedContentOrNull(note, accountViewModel) { content -> + ChannelName( + channelPicture = { + NonClickableUserPictures( + users = room.users, + accountViewModel = accountViewModel, + size = Size55dp + ) + }, + channelTitle = { + RoomNameDisplay(room, it, accountViewModel) + }, + channelLastTime = createAt, + channelLastContent = content, + hasNewMessages = hasNewMessages, + onClick = { nav(route) } + ) + } } @Composable diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt index 5cc0e1e25..8c4de4b5e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt @@ -630,17 +630,28 @@ private fun RenderRegularTextNote( nav: (String) -> Unit ) { val tags = remember(note.event) { note.event?.tags()?.toImmutableListOfLists() ?: EmptyTagList } - val eventContent by remember { mutableStateOf(accountViewModel.decrypt(note)) } val modifier = remember { Modifier.padding(top = 5.dp) } - if (eventContent != null) { - SensitivityWarning( - note = note, - accountViewModel = accountViewModel - ) { + LoadDecryptedContentOrNull(note = note, accountViewModel = accountViewModel) { eventContent -> + if (eventContent != null) { + SensitivityWarning( + note = note, + accountViewModel = accountViewModel + ) { + TranslatableRichTextViewer( + content = eventContent!!, + canPreview = canPreview, + modifier = modifier, + tags = tags, + backgroundColor = backgroundBubbleColor, + accountViewModel = accountViewModel, + nav = nav + ) + } + } else { TranslatableRichTextViewer( - content = eventContent!!, - canPreview = canPreview, + content = stringResource(id = R.string.could_not_decrypt_the_message), + canPreview = true, modifier = modifier, tags = tags, backgroundColor = backgroundBubbleColor, @@ -648,16 +659,6 @@ private fun RenderRegularTextNote( nav = nav ) } - } else { - TranslatableRichTextViewer( - content = stringResource(id = R.string.could_not_decrypt_the_message), - canPreview = true, - modifier = modifier, - tags = tags, - backgroundColor = backgroundBubbleColor, - accountViewModel = accountViewModel, - nav = nav - ) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 38d7d072d..85e9e2eb2 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -1318,6 +1318,52 @@ fun authorRouteFor(note: Note): String { return "User/${note.author?.pubkeyHex}" } +@Composable +fun LoadDecryptedContent( + note: Note, + accountViewModel: AccountViewModel, + inner: @Composable (String) -> Unit +) { + var decryptedContent by remember(note.event) { + mutableStateOf( + accountViewModel.cachedDecrypt(note) + ) + } + + decryptedContent?.let { + inner(it) + } ?: run { + LaunchedEffect(key1 = decryptedContent) { + accountViewModel.decrypt(note) { + decryptedContent = it + } + } + } +} + +@Composable +fun LoadDecryptedContentOrNull( + note: Note, + accountViewModel: AccountViewModel, + inner: @Composable (String?) -> Unit +) { + var decryptedContent by remember(note.event) { + mutableStateOf( + accountViewModel.cachedDecrypt(note) + ) + } + + if (decryptedContent == null) { + LaunchedEffect(key1 = decryptedContent) { + accountViewModel.decrypt(note) { + decryptedContent = it + } + } + } + + inner(decryptedContent) +} + @Composable fun RenderTextEvent( note: Note, @@ -1327,18 +1373,19 @@ fun RenderTextEvent( accountViewModel: AccountViewModel, nav: (String) -> Unit ) { - val eventContent = remember(note.event) { - val subject = (note.event as? TextNoteEvent)?.subject()?.ifEmpty { null } - val body = accountViewModel.decrypt(note) + LoadDecryptedContent(note, accountViewModel) { body -> + val eventContent by remember(note.event) { + derivedStateOf { + val subject = (note.event as? TextNoteEvent)?.subject()?.ifEmpty { null } - if (!subject.isNullOrBlank() && body?.split("\n")?.get(0)?.contains(subject) == false) { - "### $subject\n$body" - } else { - body + if (!subject.isNullOrBlank() && !body.split("\n")[0].contains(subject)) { + "### $subject\n$body" + } else { + body + } + } } - } - if (eventContent != null) { val isAuthorTheLoggedUser = remember(note.event) { accountViewModel.isLoggedUser(note.author) } if (makeItShort && isAuthorTheLoggedUser) { @@ -1636,17 +1683,16 @@ private fun RenderPrivateMessage( val withMe = remember { noteEvent.with(accountViewModel.userProfile().pubkeyHex) } if (withMe) { - val eventContent by remember { mutableStateOf(accountViewModel.decrypt(note)) } - val hashtags = remember(note.event?.id()) { note.event?.hashtags()?.toImmutableList() ?: persistentListOf() } - val modifier = remember(note.event?.id()) { Modifier.fillMaxWidth() } - val isAuthorTheLoggedUser = remember(note.event?.id()) { accountViewModel.isLoggedUser(note.author) } + LoadDecryptedContent(note, accountViewModel) { eventContent -> + val hashtags = remember(note.event?.id()) { note.event?.hashtags()?.toImmutableList() ?: persistentListOf() } + val modifier = remember(note.event?.id()) { Modifier.fillMaxWidth() } + val isAuthorTheLoggedUser = remember(note.event?.id()) { accountViewModel.isLoggedUser(note.author) } - val tags = remember(note) { note.event?.tags()?.toImmutableListOfLists() ?: EmptyTagList } + val tags = remember(note) { note.event?.tags()?.toImmutableListOfLists() ?: EmptyTagList } - if (eventContent != null) { if (makeItShort && isAuthorTheLoggedUser) { Text( - text = eventContent!!, + text = eventContent, color = MaterialTheme.colorScheme.placeholderText, maxLines = 2, overflow = TextOverflow.Ellipsis @@ -1657,7 +1703,7 @@ private fun RenderPrivateMessage( accountViewModel = accountViewModel ) { TranslatableRichTextViewer( - content = eventContent!!, + content = eventContent, canPreview = canPreview && !makeItShort, modifier = modifier, tags = tags, @@ -1667,7 +1713,7 @@ private fun RenderPrivateMessage( ) } - DisplayUncitedHashtags(hashtags, eventContent!!, nav) + DisplayUncitedHashtags(hashtags, eventContent, nav) } } } else { @@ -2225,19 +2271,14 @@ private fun EmojiListOptions( }.distinctUntilChanged() }.observeAsState() - Crossfade(targetState = hasAddedThis) { - val scope = rememberCoroutineScope() + Crossfade(targetState = hasAddedThis, label = "EmojiListOptions") { if (it != true) { AddButton() { - scope.launch(Dispatchers.IO) { - accountViewModel.addEmojiPack(usersEmojiList, emojiPackNote) - } + accountViewModel.addEmojiPack(usersEmojiList, emojiPackNote) } } else { RemoveButton { - scope.launch(Dispatchers.IO) { - accountViewModel.removeEmojiPack(usersEmojiList, emojiPackNote) - } + accountViewModel.removeEmojiPack(usersEmojiList, emojiPackNote) } } } @@ -2909,7 +2950,13 @@ private fun GenericRepostSection( baseAuthorPicture() } - Box(remember { Size18Modifier.align(Alignment.BottomStart).padding(1.dp) }) { + Box( + remember { + Size18Modifier + .align(Alignment.BottomStart) + .padding(1.dp) + } + ) { RepostedIcon(modifier = Size18Modifier, MaterialTheme.colorScheme.placeholderText) } @@ -3593,18 +3640,7 @@ fun AudioHeader(noteEvent: AudioHeaderEvent, note: Note, accountViewModel: Accou val defaultBackground = MaterialTheme.colorScheme.background val background = remember { mutableStateOf(defaultBackground) } - val tags = remember(noteEvent) { noteEvent?.tags()?.toImmutableListOfLists() ?: EmptyTagList } - - val eventContent = remember(note.event) { - val subject = (note.event as? TextNoteEvent)?.subject()?.ifEmpty { null } - val body = accountViewModel.decrypt(note) - - if (!subject.isNullOrBlank() && body?.split("\n")?.get(0)?.contains(subject) == false) { - "### $subject\n$body" - } else { - body - } - } + val tags = remember(noteEvent) { noteEvent.tags()?.toImmutableListOfLists() ?: EmptyTagList } Row(modifier = Modifier.padding(top = 5.dp)) { Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt index 1542cb039..bb4401bea 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt @@ -38,6 +38,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -143,8 +144,21 @@ fun NoteQuickActionMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Uni } if (showSelectTextDialog.value) { - accountViewModel.decrypt(note)?.let { - SelectTextDialog(it) { showSelectTextDialog.value = false } + val decryptedNote = remember { + mutableStateOf(null) + } + + LaunchedEffect(key1 = Unit) { + accountViewModel.decrypt(note) { + decryptedNote.value = it + } + } + + decryptedNote.value?.let { + SelectTextDialog(it) { + showSelectTextDialog.value = false + decryptedNote.value = null + } } } @@ -216,15 +230,12 @@ private fun RenderMainPopup( icon = Icons.Default.ContentCopy, label = stringResource(R.string.quick_action_copy_text) ) { - scope.launch(Dispatchers.IO) { - clipboardManager.setText( - AnnotatedString( - accountViewModel.decrypt(note) ?: "" - ) - ) + accountViewModel.decrypt(note) { + clipboardManager.setText(AnnotatedString(it)) showToast(R.string.copied_note_text_to_clipboard) - onDismiss() } + + onDismiss() } VerticalDivider(primaryLight) NoteQuickActionItem( @@ -278,10 +289,8 @@ private fun RenderMainPopup( stringResource(R.string.quick_action_delete) ) { if (accountViewModel.hideDeleteRequestDialog) { - scope.launch(Dispatchers.IO) { - accountViewModel.delete(note) - onDismiss() - } + accountViewModel.delete(note) + onDismiss() } else { showDeleteAlertDialog.value = true } @@ -380,24 +389,18 @@ fun NoteQuickActionItem(icon: ImageVector, label: String, onClick: () -> Unit) { @Composable fun DeleteAlertDialog(note: Note, accountViewModel: AccountViewModel, onDismiss: () -> Unit) { - val scope = rememberCoroutineScope() - QuickActionAlertDialog( title = stringResource(R.string.quick_action_request_deletion_alert_title), textContent = stringResource(R.string.quick_action_request_deletion_alert_body), buttonIcon = Icons.Default.Delete, buttonText = stringResource(R.string.quick_action_delete_dialog_btn), onClickDoOnce = { - scope.launch(Dispatchers.IO) { - accountViewModel.delete(note) - } + accountViewModel.delete(note) onDismiss() }, onClickDontShowAgain = { - scope.launch(Dispatchers.IO) { - accountViewModel.delete(note) - accountViewModel.dontShowDeleteRequestDialog() - } + accountViewModel.delete(note) + accountViewModel.dontShowDeleteRequestDialog() onDismiss() }, onDismiss = onDismiss diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt index 2e4cc6cc0..59a967924 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt @@ -335,7 +335,7 @@ fun ZapVote( interactionSource = remember { MutableInteractionSource() }, indication = rememberRipple(bounded = false, radius = 24.dp), onClick = { - if (!accountViewModel.isWriteable() && !accountViewModel.loggedInWithExternalSigner()) { + if (!accountViewModel.isWriteable()) { accountViewModel.toast( R.string.read_only_user, R.string.login_with_a_private_key_to_be_able_to_send_zaps diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt index 1f3ba43ac..e43e60b93 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt @@ -10,6 +10,7 @@ import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.quartz.events.CLOSED_AT import com.vitorpamplona.quartz.events.CONSENSUS_THRESHOLD import com.vitorpamplona.quartz.events.LnZapEvent +import com.vitorpamplona.quartz.events.LnZapRequestEvent import com.vitorpamplona.quartz.events.PollNoteEvent import com.vitorpamplona.quartz.events.VALUE_MAXIMUM import com.vitorpamplona.quartz.events.VALUE_MINIMUM @@ -68,10 +69,13 @@ class PollNoteViewModel : ViewModel() { fun refreshTallies() { viewModelScope.launch(Dispatchers.Default) { totalZapped = totalZapped() - wasZappedByLoggedInAccount = pollNote?.let { account?.calculateIfNoteWasZappedByAccount(it) } ?: false + wasZappedByLoggedInAccount = false + account?.calculateIfNoteWasZappedByAccount(pollNote) { + wasZappedByLoggedInAccount = true + } - val newOptions = pollOptions?.keys?.map { - val zappedInOption = zappedPollOptionAmount(it) + val newOptions = pollOptions?.keys?.map { option -> + val zappedInOption = zappedPollOptionAmount(option) val myTally = if (totalZapped.compareTo(BigDecimal.ZERO) > 0) { zappedInOption.divide(totalZapped, 2, RoundingMode.HALF_UP) @@ -79,11 +83,11 @@ class PollNoteViewModel : ViewModel() { BigDecimal.ZERO } - val zappedByLoggedIn = account?.userProfile()?.let { it1 -> isPollOptionZappedBy(it, it1) } ?: false + val cachedZappedByLoggedIn = account?.userProfile()?.let { it1 -> cachedIsPollOptionZappedBy(option, it1) } ?: false val consensus = consensusThreshold != null && myTally >= consensusThreshold!! - PollOption(it, pollOptions?.get(it) ?: "", zappedInOption, myTally, consensus, zappedByLoggedIn) + PollOption(option, pollOptions?.get(option) ?: "", zappedInOption, myTally, consensus, cachedZappedByLoggedIn) } _tallies.emit( @@ -166,11 +170,15 @@ class PollNoteViewModel : ViewModel() { return false } - fun isPollOptionZappedBy(option: Int, user: User): Boolean { + fun isPollOptionZappedBy(option: Int, user: User, onWasZappedByAuthor: () -> Unit) { + pollNote?.isZappedBy(option, user, account!!, onWasZappedByAuthor) + } + + fun cachedIsPollOptionZappedBy(option: Int, user: User): Boolean { return pollNote!!.zaps .any { val zapEvent = it.value?.event as? LnZapEvent - val privateZapAuthor = account?.decryptZapContentAuthor(it.key) + val privateZapAuthor = (it.key.event as? LnZapRequestEvent)?.cachedPrivateZap() zapEvent?.zappedPollOption() == option && (it.key.author?.pubkeyHex == user.pubkeyHex || privateZapAuthor?.pubKey == user.pubkeyHex) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt index f6acaad33..9f504d38a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt @@ -114,7 +114,6 @@ import kotlinx.collections.immutable.toImmutableMap import kotlinx.collections.immutable.toImmutableSet import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import java.math.BigDecimal import java.math.RoundingMode import java.text.DecimalFormat @@ -542,14 +541,10 @@ fun ReplyReaction( if (accountViewModel.isWriteable()) { onPress() } else { - if (accountViewModel.loggedInWithExternalSigner()) { - onPress() - } else { - accountViewModel.toast( - R.string.read_only_user, - R.string.login_with_a_private_key_to_be_able_to_reply - ) - } + accountViewModel.toast( + R.string.read_only_user, + R.string.login_with_a_private_key_to_be_able_to_reply + ) } } ) { @@ -849,14 +844,10 @@ private fun likeClick( R.string.no_reaction_type_setup_long_press_to_change ) } else if (!accountViewModel.isWriteable()) { - if (accountViewModel.loggedInWithExternalSigner()) { - onWantsToSignReaction() - } else { - accountViewModel.toast( - R.string.read_only_user, - R.string.login_with_a_private_key_to_like_posts - ) - } + accountViewModel.toast( + R.string.read_only_user, + R.string.login_with_a_private_key_to_like_posts + ) } else if (accountViewModel.account.reactionChoices.size == 1) { accountViewModel.reactToOrDelete(baseNote) } else if (accountViewModel.account.reactionChoices.size > 1) { @@ -1067,7 +1058,7 @@ private fun zapClick( context.getString(R.string.error_dialog_zap_error), context.getString(R.string.no_zap_amount_setup_long_press_to_change) ) - } else if (!accountViewModel.isWriteable() && !accountViewModel.loggedInWithExternalSigner()) { + } else if (!accountViewModel.isWriteable()) { accountViewModel.toast( context.getString(R.string.error_dialog_zap_error), context.getString(R.string.login_with_a_private_key_to_be_able_to_send_zaps) @@ -1128,9 +1119,7 @@ private fun ObserveZapAmountText( LaunchedEffect(key1 = zapsState) { accountViewModel.calculateZapAmount(baseNote) { newZapAmount -> if (zapAmountTxt.value != newZapAmount) { - withContext(Dispatchers.Main) { - zapAmountTxt.value = newZapAmount - } + zapAmountTxt.value = newZapAmount } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt index d3dd6db22..732396b17 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt @@ -46,13 +46,11 @@ import androidx.lifecycle.map import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.ExternalSignerUtils import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImage import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.ReportNoteDialog import com.vitorpamplona.quartz.encoders.HexKey -import com.vitorpamplona.quartz.encoders.toHexKey import kotlinx.collections.immutable.ImmutableSet import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -438,7 +436,9 @@ fun NoteDropDownMenu(note: Note, popupExpanded: MutableState, accountVi }, onClick = { scope.launch(Dispatchers.IO) { - clipboardManager.setText(AnnotatedString(accountViewModel.decrypt(note) ?: "")) + accountViewModel.decrypt(note) { + clipboardManager.setText(AnnotatedString(it)) + } onDismiss() } } @@ -501,21 +501,8 @@ fun NoteDropDownMenu(note: Note, popupExpanded: MutableState, accountVi Text(stringResource(R.string.remove_from_private_bookmarks)) }, onClick = { - scope.launch(Dispatchers.IO) { - if (accountViewModel.loggedInWithExternalSigner()) { - val bookmarks = accountViewModel.userProfile().latestBookmarkList - ExternalSignerUtils.decrypt( - bookmarks?.content ?: "", - accountViewModel.account.keyPair.pubKey.toHexKey(), - bookmarks?.id ?: "" - ) - bookmarks?.decryptedContent = ExternalSignerUtils.cachedDecryptedContent[bookmarks?.id ?: ""] ?: "" - accountViewModel.removePrivateBookmark(note, bookmarks?.decryptedContent ?: "") - } else { - accountViewModel.removePrivateBookmark(note) - onDismiss() - } - } + accountViewModel.removePrivateBookmark(note) + onDismiss() } ) } else { @@ -524,21 +511,8 @@ fun NoteDropDownMenu(note: Note, popupExpanded: MutableState, accountVi Text(stringResource(R.string.add_to_private_bookmarks)) }, onClick = { - scope.launch(Dispatchers.IO) { - if (accountViewModel.loggedInWithExternalSigner()) { - val bookmarks = accountViewModel.userProfile().latestBookmarkList - ExternalSignerUtils.decrypt( - bookmarks?.content ?: "", - accountViewModel.account.keyPair.pubKey.toHexKey(), - bookmarks?.id ?: "" - ) - bookmarks?.decryptedContent = ExternalSignerUtils.cachedDecryptedContent[bookmarks?.id ?: ""] ?: "" - accountViewModel.addPrivateBookmark(note, bookmarks?.decryptedContent ?: "") - } else { - accountViewModel.addPrivateBookmark(note) - onDismiss() - } - } + accountViewModel.addPrivateBookmark(note) + onDismiss() } ) } @@ -548,24 +522,8 @@ fun NoteDropDownMenu(note: Note, popupExpanded: MutableState, accountVi Text(stringResource(R.string.remove_from_public_bookmarks)) }, onClick = { - scope.launch(Dispatchers.IO) { - if (accountViewModel.loggedInWithExternalSigner()) { - val bookmarks = accountViewModel.userProfile().latestBookmarkList - ExternalSignerUtils.decrypt( - bookmarks?.content ?: "", - accountViewModel.account.keyPair.pubKey.toHexKey(), - bookmarks?.id ?: "" - ) - bookmarks?.decryptedContent = ExternalSignerUtils.cachedDecryptedContent[bookmarks?.id ?: ""] ?: "" - accountViewModel.removePublicBookmark( - note, - bookmarks?.decryptedContent ?: "" - ) - } else { - accountViewModel.removePublicBookmark(note) - onDismiss() - } - } + accountViewModel.removePublicBookmark(note) + onDismiss() } ) } else { @@ -574,24 +532,8 @@ fun NoteDropDownMenu(note: Note, popupExpanded: MutableState, accountVi Text(stringResource(R.string.add_to_public_bookmarks)) }, onClick = { - scope.launch(Dispatchers.IO) { - if (accountViewModel.loggedInWithExternalSigner()) { - val bookmarks = accountViewModel.userProfile().latestBookmarkList - ExternalSignerUtils.decrypt( - bookmarks?.content ?: "", - accountViewModel.account.keyPair.pubKey.toHexKey(), - bookmarks?.id ?: "" - ) - bookmarks?.decryptedContent = ExternalSignerUtils.cachedDecryptedContent[bookmarks?.id ?: ""] ?: "" - accountViewModel.addPublicBookmark( - note, - bookmarks?.decryptedContent ?: "" - ) - } else { - accountViewModel.addPublicBookmark(note) - onDismiss() - } - } + accountViewModel.addPublicBookmark(note) + onDismiss() } ) } @@ -654,19 +596,21 @@ fun WatchBookmarksFollowsAndAccount(note: Note, accountViewModel: AccountViewMod LaunchedEffect(key1 = followState, key2 = bookmarkState, key3 = showSensitiveContent) { launch(Dispatchers.IO) { - val newState = DropDownParams( - isFollowingAuthor = accountViewModel.isFollowing(note.author), - isPrivateBookmarkNote = accountViewModel.isInPrivateBookmarks(note), - isPublicBookmarkNote = accountViewModel.isInPublicBookmarks(note), - isLoggedUser = accountViewModel.isLoggedUser(note.author), - isSensitive = note.event?.isSensitive() ?: false, - showSensitiveContent = showSensitiveContent - ) - - launch(Dispatchers.Main) { - onNew( - newState + accountViewModel.isInPrivateBookmarks(note) { + val newState = DropDownParams( + isFollowingAuthor = accountViewModel.isFollowing(note.author), + isPrivateBookmarkNote = it, + isPublicBookmarkNote = accountViewModel.isInPublicBookmarks(note), + isLoggedUser = accountViewModel.isLoggedUser(note.author), + isSensitive = note.event?.isSensitive() ?: false, + showSensitiveContent = showSensitiveContent ) + + launch(Dispatchers.Main) { + onNew( + newState + ) + } } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt index d633902b6..aae9d426f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt @@ -198,14 +198,10 @@ fun ShowFollowingOrUnfollowingButton( if (isFollowing) { UnfollowButton { if (!accountViewModel.isWriteable()) { - if (accountViewModel.loggedInWithExternalSigner()) { - accountViewModel.unfollow(baseAuthor) - } else { - accountViewModel.toast( - R.string.read_only_user, - R.string.login_with_a_private_key_to_be_able_to_unfollow - ) - } + accountViewModel.toast( + R.string.read_only_user, + R.string.login_with_a_private_key_to_be_able_to_unfollow + ) } else { accountViewModel.unfollow(baseAuthor) } @@ -213,14 +209,10 @@ fun ShowFollowingOrUnfollowingButton( } else { FollowButton { if (!accountViewModel.isWriteable()) { - if (accountViewModel.loggedInWithExternalSigner()) { - accountViewModel.follow(baseAuthor) - } else { - accountViewModel.toast( - R.string.read_only_user, - R.string.login_with_a_private_key_to_be_able_to_follow - ) - } + accountViewModel.toast( + R.string.read_only_user, + R.string.login_with_a_private_key_to_be_able_to_follow + ) } else { accountViewModel.follow(baseAuthor) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountScreen.kt index 3d6c804ee..3eadfda89 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountScreen.kt @@ -1,5 +1,9 @@ package com.vitorpamplona.amethyst.ui.screen +import android.app.Activity +import android.util.Log +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.Crossfade import androidx.compose.animation.core.tween import androidx.compose.foundation.layout.Arrangement @@ -9,6 +13,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment @@ -19,12 +24,18 @@ import androidx.lifecycle.ViewModelStoreOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner import androidx.lifecycle.viewmodel.compose.viewModel +import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ServiceManager import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.ui.MainActivity +import com.vitorpamplona.amethyst.ui.components.getActivity import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.MainScreen import com.vitorpamplona.amethyst.ui.screen.loggedOff.LoginPage +import com.vitorpamplona.quartz.signers.NostrSignerExternal +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch @Composable fun AccountScreen( @@ -44,15 +55,19 @@ fun AccountScreen( LoadingAccounts() } is AccountState.LoggedOff -> { - LaunchedEffect(key1 = accountState) { - serviceManager.pauseForGood() + LaunchedEffect(key1 = state) { + launch(Dispatchers.IO) { + serviceManager.pauseForGood() + } } LoginPage(accountStateViewModel, isFirstLogin = true) } is AccountState.LoggedIn -> { - LaunchedEffect(key1 = accountState) { - serviceManager.restartIfDifferentAccount(state.account) + LaunchedEffect(key1 = state) { + launch(Dispatchers.IO) { + serviceManager.restartIfDifferentAccount(state.account) + } } CompositionLocalProvider( @@ -66,8 +81,10 @@ fun AccountScreen( } } is AccountState.LoggedInViewOnly -> { - LaunchedEffect(key1 = accountState) { - serviceManager.restartIfDifferentAccount(state.account) + LaunchedEffect(key1 = state) { + launch(Dispatchers.IO) { + serviceManager.restartIfDifferentAccount(state.account) + } } CompositionLocalProvider( @@ -98,6 +115,51 @@ fun LoggedInPage( ) ) + val activity = getActivity() as MainActivity + // Find a better way to associate these two. + accountViewModel.serviceManager = activity.serviceManager + + if (accountViewModel.account.signer is NostrSignerExternal) { + val launcher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartActivityForResult(), + onResult = { result -> + if (result.resultCode != Activity.RESULT_OK) { + accountViewModel.toast( + R.string.sign_request_rejected, + R.string.sign_request_rejected_description + ) + } else { + result.data?.let { + accountViewModel.runOnIO { + accountViewModel.account.signer.launcher.newResult(it) + } + } + } + } + ) + + DisposableEffect(accountViewModel, accountViewModel.account, launcher, activity) { + accountViewModel.account.signer.launcher.registerLauncher( + launcher = { + try { + activity.prepareToLaunchSigner() + launcher.launch(it) + } catch (e: Exception) { + Log.e("Signer", "Error opening Signer app", e) + accountViewModel.toast( + R.string.error_opening_external_signer, + R.string.error_opening_external_signer_description + ) + } + }, + contentResolver = { Amethyst.instance.contentResolver } + ) + onDispose { + accountViewModel.account.signer.launcher.clearLauncher() + } + } + } + MainScreen(accountViewModel, accountStateViewModel, sharedPreferencesViewModel) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt index f96cf6c3f..979e7fb47 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt @@ -13,6 +13,9 @@ import com.vitorpamplona.quartz.encoders.Hex import com.vitorpamplona.quartz.encoders.Nip19 import com.vitorpamplona.quartz.encoders.bechToBytes import com.vitorpamplona.quartz.encoders.hexToByteArray +import com.vitorpamplona.quartz.encoders.toHexKey +import com.vitorpamplona.quartz.signers.NostrSignerExternal +import com.vitorpamplona.quartz.signers.NostrSignerInternal import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -49,7 +52,12 @@ class AccountStateViewModel() : ViewModel() { _accountContent.update { AccountState.LoggedOff } } - suspend fun loginAndStartUI(key: String, useProxy: Boolean, proxyPort: Int, loginWithExternalSigner: Boolean = false) = withContext(Dispatchers.IO) { + suspend fun loginAndStartUI( + key: String, + useProxy: Boolean, + proxyPort: Int, + loginWithExternalSigner: Boolean = false + ) = withContext(Dispatchers.IO) { val parsed = Nip19.uriToRoute(key) val pubKeyParsed = parsed?.hex?.hexToByteArray() val proxy = HttpClient.initProxy(useProxy, "127.0.0.1", proxyPort) @@ -60,16 +68,21 @@ class AccountStateViewModel() : ViewModel() { val account = if (loginWithExternalSigner) { - Account(KeyPair(pubKey = pubKeyParsed), proxy = proxy, proxyPort = proxyPort, loginWithExternalSigner = true) + val keyPair = KeyPair(pubKey = pubKeyParsed) + Account(keyPair, proxy = proxy, proxyPort = proxyPort, signer = NostrSignerExternal(keyPair.pubKey.toHexKey())) } else if (key.startsWith("nsec")) { - Account(KeyPair(privKey = key.bechToBytes()), proxy = proxy, proxyPort = proxyPort) + val keyPair = KeyPair(privKey = key.bechToBytes()) + Account(keyPair, proxy = proxy, proxyPort = proxyPort, signer = NostrSignerInternal(keyPair)) } else if (pubKeyParsed != null) { - Account(KeyPair(pubKey = pubKeyParsed), proxy = proxy, proxyPort = proxyPort) + val keyPair = KeyPair(pubKey = pubKeyParsed) + Account(keyPair, proxy = proxy, proxyPort = proxyPort, signer = NostrSignerInternal(keyPair)) } else if (EMAIL_PATTERN.matcher(key).matches()) { + val keyPair = KeyPair() // Evaluate NIP-5 - Account(KeyPair(), proxy = proxy, proxyPort = proxyPort) + Account(keyPair, proxy = proxy, proxyPort = proxyPort, signer = NostrSignerInternal(keyPair)) } else { - Account(KeyPair(Hex.decode(key)), proxy = proxy, proxyPort = proxyPort) + val keyPair = KeyPair(Hex.decode(key)) + Account(keyPair, proxy = proxy, proxyPort = proxyPort, signer = NostrSignerInternal(keyPair)) } LocalPreferences.updatePrefsForLogin(account) @@ -78,7 +91,7 @@ class AccountStateViewModel() : ViewModel() { } suspend fun startUI(account: Account) = withContext(Dispatchers.Main) { - if (account.keyPair.privKey != null) { + if (account.isWriteable()) { _accountContent.update { AccountState.LoggedIn(account) } } else { _accountContent.update { AccountState.LoggedInViewOnly(account) } @@ -132,7 +145,8 @@ class AccountStateViewModel() : ViewModel() { fun newKey(useProxy: Boolean, proxyPort: Int) { viewModelScope.launch(Dispatchers.IO) { val proxy = HttpClient.initProxy(useProxy, "127.0.0.1", proxyPort) - val account = Account(KeyPair(), proxy = proxy, proxyPort = proxyPort) + val keyPair = KeyPair() + val account = Account(keyPair, proxy = proxy, proxyPort = proxyPort, signer = NostrSignerInternal(keyPair)) account.follow(account.userProfile()) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt index 950f54dc7..e46d3211a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt @@ -191,8 +191,23 @@ class NostrHomeRepliesFeedViewModel(val account: Account) : FeedViewModel(HomeCo } } -class NostrBookmarkPublicFeedViewModel : FeedViewModel(BookmarkPublicFeedFilter) -class NostrBookmarkPrivateFeedViewModel : FeedViewModel(BookmarkPrivateFeedFilter) +@Stable +class NostrBookmarkPublicFeedViewModel(val account: Account) : FeedViewModel(BookmarkPublicFeedFilter(account)) { + class Factory(val account: Account) : ViewModelProvider.Factory { + override fun create(modelClass: Class): NostrBookmarkPublicFeedViewModel { + return NostrBookmarkPublicFeedViewModel(account) as NostrBookmarkPublicFeedViewModel + } + } +} + +@Stable +class NostrBookmarkPrivateFeedViewModel(val account: Account) : FeedViewModel(BookmarkPrivateFeedFilter(account)) { + class Factory(val account: Account) : ViewModelProvider.Factory { + override fun create(modelClass: Class): NostrBookmarkPrivateFeedViewModel { + return NostrBookmarkPrivateFeedViewModel(account) as NostrBookmarkPrivateFeedViewModel + } + } +} class NostrUserAppRecommendationsFeedViewModel(val user: User) : FeedViewModel(UserProfileAppRecommendationsFeedFilter(user)) { class Factory(val user: User) : ViewModelProvider.Factory { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 1677e37d6..93be5ec7b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -4,6 +4,7 @@ import android.content.Context import android.graphics.drawable.Drawable import android.util.Log import androidx.compose.runtime.Immutable +import androidx.compose.runtime.MutableState import androidx.compose.runtime.Stable import androidx.compose.runtime.mutableIntStateOf import androidx.lifecycle.LiveData @@ -15,6 +16,7 @@ import androidx.lifecycle.viewModelScope import coil.imageLoader import coil.request.ImageRequest import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ServiceManager import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.AccountState import com.vitorpamplona.amethyst.model.AddressableNote @@ -25,6 +27,7 @@ import com.vitorpamplona.amethyst.model.RelayInformation import com.vitorpamplona.amethyst.model.UrlCachedPreviewer import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.UserState +import com.vitorpamplona.amethyst.service.HttpClient import com.vitorpamplona.amethyst.service.Nip05Verifier import com.vitorpamplona.amethyst.service.Nip11CachedRetriever import com.vitorpamplona.amethyst.service.Nip11Retriever @@ -68,7 +71,6 @@ import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch -import java.math.BigDecimal import java.util.Locale import kotlin.time.measureTimedValue @@ -92,21 +94,7 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View val toasts = MutableSharedFlow(0, 3, onBufferOverflow = BufferOverflow.DROP_OLDEST) - val discoveryListLiveData = account.live.map { - it.account.defaultDiscoveryFollowList - }.distinctUntilChanged() - - val homeListLiveData = account.live.map { - it.account.defaultHomeFollowList - }.distinctUntilChanged() - - val notificationListLiveData = account.live.map { - it.account.defaultNotificationFollowList - }.distinctUntilChanged() - - val storiesListLiveData = account.live.map { - it.account.defaultStoriesFollowList - }.distinctUntilChanged() + var serviceManager: ServiceManager? = null val showSensitiveContentChanges = account.live.map { it.account.showSensitiveContent @@ -134,15 +122,11 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View return account.isWriteable() } - fun loggedInWithExternalSigner(): Boolean { - return account.loginWithExternalSigner - } - fun userProfile(): User { return account.userProfile() } - fun reactTo(note: Note, reaction: String) { + suspend fun reactTo(note: Note, reaction: String) { account.reactTo(note, reaction) } @@ -177,7 +161,7 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View return account.hasReacted(baseNote, reaction) } - fun deleteReactionTo(note: Note, reaction: String) { + suspend fun deleteReactionTo(note: Note, reaction: String) { account.delete(account.reactionTo(note, reaction)) } @@ -193,18 +177,18 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View fun calculateIfNoteWasZappedByAccount(zappedNote: Note, onWasZapped: (Boolean) -> Unit) { viewModelScope.launch(Dispatchers.Default) { - onWasZapped(account.calculateIfNoteWasZappedByAccount(zappedNote)) + account.calculateIfNoteWasZappedByAccount(zappedNote) { + onWasZapped(true) + } } } - suspend fun calculateZapAmount(zappedNote: Note): BigDecimal { - return account.calculateZappedAmount(zappedNote) - } - - suspend fun calculateZapAmount(zappedNote: Note, onZapAmount: suspend (String) -> Unit) { + fun calculateZapAmount(zappedNote: Note, onZapAmount: (String) -> Unit) { if (zappedNote.zapPayments.isNotEmpty()) { viewModelScope.launch(Dispatchers.IO) { - onZapAmount(showAmount(account.calculateZappedAmount(zappedNote))) + account.calculateZappedAmount(zappedNote) { + onZapAmount(showAmount(it)) + } } } else { onZapAmount(showAmount(zappedNote.zapsAmount)) @@ -214,20 +198,21 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View fun calculateZapraiser(zappedNote: Note, onZapraiserStatus: (ZapraiserStatus) -> Unit) { viewModelScope.launch(Dispatchers.IO) { val zapraiserAmount = zappedNote.event?.zapraiserAmount() ?: 0 - val newZapAmount = calculateZapAmount(zappedNote) - var percentage = newZapAmount.div(zapraiserAmount.toBigDecimal()).toFloat() + account.calculateZappedAmount(zappedNote) { newZapAmount -> + var percentage = newZapAmount.div(zapraiserAmount.toBigDecimal()).toFloat() - if (percentage > 1) { - percentage = 1f - } + if (percentage > 1) { + percentage = 1f + } - val newZapraiserProgress = percentage - val newZapraiserLeft = if (percentage > 0.99) { - "0" - } else { - showAmount((zapraiserAmount * (1 - percentage)).toBigDecimal()) + val newZapraiserProgress = percentage + val newZapraiserLeft = if (percentage > 0.99) { + "0" + } else { + showAmount((zapraiserAmount * (1 - percentage)).toBigDecimal()) + } + onZapraiserStatus(ZapraiserStatus(newZapraiserProgress, newZapraiserLeft)) } - onZapraiserStatus(ZapraiserStatus(newZapraiserProgress, newZapraiserLeft)) } } @@ -236,14 +221,14 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View onNewState: (ImmutableList) -> Unit ) { viewModelScope.launch(Dispatchers.IO) { - val list = ArrayList(zaps.size) - zaps.forEach { - innerDecryptAmountMessage(it.request, it.response)?.let { - list.add(it) + allOrNothingSigningOperations( + remainingTos = zaps, + runRequestFor = { next, onReady -> + innerDecryptAmountMessage(next.request, next.response, onReady) } + ) { + onNewState(it.toImmutableList()) } - - onNewState(list.toImmutableList()) } } @@ -252,14 +237,14 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View onNewState: (ImmutableList) -> Unit ) { viewModelScope.launch(Dispatchers.IO) { - val list = ArrayList(baseNote.zaps.size) - baseNote.zaps.forEach { - innerDecryptAmountMessage(it.key, it.value)?.let { - list.add(it) + allOrNothingSigningOperations, ZapAmountCommentNotification>( + remainingTos = baseNote.zaps.toList(), + runRequestFor = { next, onReady -> + innerDecryptAmountMessage(next.first, next.second, onReady) } + ) { + onNewState(it.toImmutableList()) } - - onNewState(list.toImmutableList()) } } @@ -269,37 +254,43 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View onNewState: (ZapAmountCommentNotification?) -> Unit ) { viewModelScope.launch(Dispatchers.IO) { - onNewState(innerDecryptAmountMessage(zapRequest, zapEvent)) + innerDecryptAmountMessage(zapRequest, zapEvent, onNewState) } } - private suspend fun innerDecryptAmountMessage( + private fun innerDecryptAmountMessage( zapRequest: Note, - zapEvent: Note? - ): ZapAmountCommentNotification? { + zapEvent: Note?, + onReady: (ZapAmountCommentNotification) -> Unit + ) { checkNotInMainThread() (zapRequest.event as? LnZapRequestEvent)?.let { - val decryptedContent = decryptZap(zapRequest) - val amount = (zapEvent?.event as? LnZapEvent)?.amount - if (decryptedContent != null) { - val newAuthor = LocalCache.getOrCreateUser(decryptedContent.pubKey) - return ZapAmountCommentNotification( - newAuthor, - decryptedContent.content.ifBlank { null }, - showAmountAxis(amount) - ) + if (it.isPrivateZap()) { + decryptZap(zapRequest) { decryptedContent -> + val amount = (zapEvent?.event as? LnZapEvent)?.amount + val newAuthor = LocalCache.getOrCreateUser(decryptedContent.pubKey) + onReady( + ZapAmountCommentNotification( + newAuthor, + decryptedContent.content.ifBlank { null }, + showAmountAxis(amount) + ) + ) + } } else { + val amount = (zapEvent?.event as? LnZapEvent)?.amount if (!zapRequest.event?.content().isNullOrBlank() || amount != null) { - return ZapAmountCommentNotification( - zapRequest.author, - zapRequest.event?.content()?.ifBlank { null }, - showAmountAxis(amount) + onReady( + ZapAmountCommentNotification( + zapRequest.author, + zapRequest.event?.content()?.ifBlank { null }, + showAmountAxis(amount) + ) ) } } } - return null } fun zap( @@ -319,7 +310,9 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View } fun report(note: Note, type: ReportEvent.ReportType, content: String = "") { - account.report(note, type, content) + viewModelScope.launch(Dispatchers.IO) { + account.report(note, type, content) + } } fun report(user: User, type: ReportEvent.ReportType) { @@ -336,47 +329,43 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View } fun removeEmojiPack(usersEmojiList: Note, emojiList: Note) { - account.removeEmojiPack(usersEmojiList, emojiList) + viewModelScope.launch(Dispatchers.IO) { + account.removeEmojiPack(usersEmojiList, emojiList) + } } fun addEmojiPack(usersEmojiList: Note, emojiList: Note) { - account.addEmojiPack(usersEmojiList, emojiList) + viewModelScope.launch(Dispatchers.IO) { + account.addEmojiPack(usersEmojiList, emojiList) + } } fun addPrivateBookmark(note: Note) { - account.addPrivateBookmark(note) - } - - fun addPrivateBookmark(note: Note, decryptedContent: String) { - account.addPrivateBookmark(note, decryptedContent) - } - - fun addPublicBookmark(note: Note, decryptedContent: String) { - account.addPublicBookmark(note, decryptedContent) - } - - fun removePublicBookmark(note: Note, decryptedContent: String) { - account.removePublicBookmark(note, decryptedContent) + viewModelScope.launch(Dispatchers.IO) { + account.addBookmark(note, true) + } } fun addPublicBookmark(note: Note) { - account.addPublicBookmark(note) - } - - fun removePrivateBookmark(note: Note, decryptedContent: String) { - account.removePrivateBookmark(note, decryptedContent) + viewModelScope.launch(Dispatchers.IO) { + account.addBookmark(note, false) + } } fun removePrivateBookmark(note: Note) { - account.removePrivateBookmark(note) + viewModelScope.launch(Dispatchers.IO) { + account.removeBookmark(note, true) + } } fun removePublicBookmark(note: Note) { - account.removePublicBookmark(note) + viewModelScope.launch(Dispatchers.IO) { + account.removeBookmark(note, false) + } } - fun isInPrivateBookmarks(note: Note): Boolean { - return account.isInPrivateBookmarks(note) + fun isInPrivateBookmarks(note: Note, onReady: (Boolean) -> Unit) { + account.isInPrivateBookmarks(note, onReady) } fun isInPublicBookmarks(note: Note): Boolean { @@ -388,15 +377,23 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View } fun delete(note: Note) { - account.delete(note) + viewModelScope.launch(Dispatchers.IO) { + account.delete(note) + } } - fun decrypt(note: Note): String? { - return account.decryptContent(note) + fun cachedDecrypt(note: Note): String? { + return account.cachedDecryptContent(note) } - fun decryptZap(note: Note): Event? { - return account.decryptZapContentAuthor(note) + fun decrypt(note: Note, onReady: (String) -> Unit) { + viewModelScope.launch(Dispatchers.IO) { + account.decryptContent(note, onReady) + } + } + + fun decryptZap(note: Note, onReady: (Event) -> Unit) { + account.decryptZapContentAuthor(note, onReady) } fun translateTo(lang: Locale) { @@ -476,7 +473,9 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View get() = account.hideDeleteRequestDialog fun dontShowDeleteRequestDialog() { - account.setHideDeleteRequestDialog() + viewModelScope.launch(Dispatchers.IO) { + account.setHideDeleteRequestDialog() + } } val hideNIP24WarningDialog: Boolean @@ -551,11 +550,11 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View } } - fun unwrap(event: GiftWrapEvent): Event? { - return account.unwrap(event) + fun unwrap(event: GiftWrapEvent, onReady: (Event) -> Unit) { + account.unwrap(event, onReady) } - fun unseal(event: SealedGossipEvent): Event? { - return account.unseal(event) + fun unseal(event: SealedGossipEvent, onReady: (Event) -> Unit) { + account.unseal(event, onReady) } fun show(user: User) { @@ -859,6 +858,18 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View } } + fun enableTor( + checked: Boolean, + portNumber: MutableState + ) { + viewModelScope.launch(Dispatchers.IO) { + account.proxyPort = portNumber.value.toInt() + account.proxy = HttpClient.initProxy(checked, "127.0.0.1", account.proxyPort) + account.saveable.invalidateData() + serviceManager?.forceRestart() + } + } + class Factory(val account: Account, val settings: SettingsState) : ViewModelProvider.Factory { override fun create(modelClass: Class): AccountViewModel { return AccountViewModel(account, settings) as AccountViewModel @@ -866,7 +877,7 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View } private var collectorJob: Job? = null - val notificationDots = HasNotificationDot(bottomNavigationItems, account) + val notificationDots = HasNotificationDot(bottomNavigationItems) private val bundlerInsert = BundledInsert>(3000, Dispatchers.IO) fun invalidateInsertData(newItems: Set) { @@ -875,9 +886,9 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View } } - suspend fun updateNotificationDots(newNotes: Set = emptySet()) { + fun updateNotificationDots(newNotes: Set = emptySet()) { val (value, elapsed) = measureTimedValue { - notificationDots.update(newNotes) + notificationDots.update(newNotes, account) } Log.d("Rendering Metrics", "Notification Dots Calculation in $elapsed for ${newNotes.size} new notes") } @@ -886,7 +897,7 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View Log.d("Init", "AccountViewModel") collectorJob = viewModelScope.launch(Dispatchers.IO) { LocalCache.live.newEventBundles.collect { newNotes -> - Log.d("Rendering Metrics", "Notification Dots Calculation refresh ${this@AccountViewModel}") + Log.d("Rendering Metrics", "Notification Dots Calculation refresh ${this@AccountViewModel} for ${account.userProfile().toBestDisplayName()}") invalidateInsertData(newNotes) } } @@ -936,26 +947,18 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View onMore() } } else { - if (loggedInWithExternalSigner()) { - if (hasBoosted(baseNote)) { - deleteBoostsTo(baseNote) - } else { - onMore() - } - } else { - toast( - R.string.read_only_user, - R.string.login_with_a_private_key_to_be_able_to_boost_posts - ) - } + toast( + R.string.read_only_user, + R.string.login_with_a_private_key_to_be_able_to_boost_posts + ) } } } -class HasNotificationDot(bottomNavigationItems: ImmutableList, val account: Account) { +class HasNotificationDot(bottomNavigationItems: ImmutableList) { val hasNewItems = bottomNavigationItems.associateWith { MutableStateFlow(false) } - fun update(newNotes: Set) { + fun update(newNotes: Set, account: Account) { checkNotInMainThread() hasNewItems.forEach { @@ -972,3 +975,22 @@ class HasNotificationDot(bottomNavigationItems: ImmutableList, val accoun @Immutable data class LoadedBechLink(val baseNote: Note?, val nip19: Nip19.Return) + +public fun allOrNothingSigningOperations( + remainingTos: List, + runRequestFor: (T, (K) -> Unit) -> Unit, + output: MutableList = mutableListOf(), + onReady: (List) -> Unit +) { + if (remainingTos.isEmpty()) { + onReady(output) + return + } + + val next = remainingTos.first() + + runRequestFor(next) { result: K -> + output.add(result) + allOrNothingSigningOperations(remainingTos.minus(next), runRequestFor, output, onReady) + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/BookmarkListScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/BookmarkListScreen.kt index f7c6c57d6..7d3da7072 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/BookmarkListScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/BookmarkListScreen.kt @@ -18,8 +18,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.ui.dal.BookmarkPrivateFeedFilter -import com.vitorpamplona.amethyst.ui.dal.BookmarkPublicFeedFilter import com.vitorpamplona.amethyst.ui.screen.NostrBookmarkPrivateFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrBookmarkPublicFeedViewModel import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView @@ -29,13 +27,17 @@ import kotlinx.coroutines.launch @OptIn(ExperimentalFoundationApi::class) @Composable fun BookmarkListScreen(accountViewModel: AccountViewModel, nav: (String) -> Unit) { - BookmarkPublicFeedFilter.account = accountViewModel.account - BookmarkPrivateFeedFilter.account = accountViewModel.account + val publicFeedViewModel: NostrBookmarkPublicFeedViewModel = viewModel( + key = "NotificationViewModel", + factory = NostrBookmarkPublicFeedViewModel.Factory(accountViewModel.account) + ) - val publicFeedViewModel: NostrBookmarkPublicFeedViewModel = viewModel() - val privateFeedViewModel: NostrBookmarkPrivateFeedViewModel = viewModel() + val privateFeedViewModel: NostrBookmarkPrivateFeedViewModel = viewModel( + key = "NotificationViewModel", + factory = NostrBookmarkPrivateFeedViewModel.Factory(accountViewModel.account) + ) - val userState by accountViewModel.account.userProfile().live().bookmarks.observeAsState() + val userState by accountViewModel.account.decryptBookmarks.observeAsState() LaunchedEffect(userState) { publicFeedViewModel.invalidateData() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DiscoverScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DiscoverScreen.kt index 885a7927e..596871b5f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DiscoverScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DiscoverScreen.kt @@ -25,7 +25,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -173,7 +172,8 @@ private fun RenderDiscoverFeed( Crossfade( targetState = feedState, - animationSpec = tween(durationMillis = 100) + animationSpec = tween(durationMillis = 100), + label = "RenderDiscoverFeed" ) { state -> when (state) { is FeedState.Empty -> { @@ -213,9 +213,9 @@ fun WatchAccountForDiscoveryScreen( discoveryChatFeedViewModel: NostrDiscoverChatFeedViewModel, accountViewModel: AccountViewModel ) { - val accountState by accountViewModel.accountLiveData.observeAsState() + val listState by accountViewModel.account.liveStoriesFollowLists.collectAsStateWithLifecycle() - LaunchedEffect(accountViewModel, accountState?.account?.defaultDiscoveryFollowList) { + LaunchedEffect(accountViewModel, listState) { NostrDiscoveryDataSource.resetFilters() discoveryLiveFeedViewModel.checkKeysInvalidateDataAndSendToTop() discoveryCommunityFeedViewModel.checkKeysInvalidateDataAndSendToTop() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/GeoHashScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/GeoHashScreen.kt index d4d9c6fad..542e2cde2 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/GeoHashScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/GeoHashScreen.kt @@ -169,14 +169,10 @@ fun GeoHashActionOptions( if (isFollowingTag) { UnfollowButton { if (!accountViewModel.isWriteable()) { - if (accountViewModel.loggedInWithExternalSigner()) { - accountViewModel.unfollowGeohash(tag) - } else { - accountViewModel.toast( - R.string.read_only_user, - R.string.login_with_a_private_key_to_be_able_to_unfollow - ) - } + accountViewModel.toast( + R.string.read_only_user, + R.string.login_with_a_private_key_to_be_able_to_unfollow + ) } else { accountViewModel.unfollowGeohash(tag) } @@ -184,14 +180,10 @@ fun GeoHashActionOptions( } else { FollowButton { if (!accountViewModel.isWriteable()) { - if (accountViewModel.loggedInWithExternalSigner()) { - accountViewModel.followGeohash(tag) - } else { - accountViewModel.toast( - R.string.read_only_user, - R.string.login_with_a_private_key_to_be_able_to_follow - ) - } + accountViewModel.toast( + R.string.read_only_user, + R.string.login_with_a_private_key_to_be_able_to_follow + ) } else { accountViewModel.followGeohash(tag) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HashtagScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HashtagScreen.kt index f2edb1c08..a9a4e463f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HashtagScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HashtagScreen.kt @@ -142,14 +142,10 @@ fun HashtagActionOptions( if (isFollowingTag) { UnfollowButton { if (!accountViewModel.isWriteable()) { - if (accountViewModel.loggedInWithExternalSigner()) { - accountViewModel.unfollowHashtag(tag) - } else { - accountViewModel.toast( - R.string.read_only_user, - R.string.login_with_a_private_key_to_be_able_to_unfollow - ) - } + accountViewModel.toast( + R.string.read_only_user, + R.string.login_with_a_private_key_to_be_able_to_unfollow + ) } else { accountViewModel.unfollowHashtag(tag) } @@ -157,14 +153,10 @@ fun HashtagActionOptions( } else { FollowButton { if (!accountViewModel.isWriteable()) { - if (accountViewModel.loggedInWithExternalSigner()) { - accountViewModel.followHashtag(tag) - } else { - accountViewModel.toast( - R.string.read_only_user, - R.string.login_with_a_private_key_to_be_able_to_follow - ) - } + accountViewModel.toast( + R.string.read_only_user, + R.string.login_with_a_private_key_to_be_able_to_follow + ) } else { accountViewModel.followHashtag(tag) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HiddenUsersScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HiddenUsersScreen.kt index 95ff37cf3..0c6342de6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HiddenUsersScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HiddenUsersScreen.kt @@ -305,14 +305,10 @@ fun MutedWordActionOptions( if (isMutedWord == true) { ShowWordButton { if (!accountViewModel.isWriteable()) { - if (accountViewModel.loggedInWithExternalSigner()) { - accountViewModel.showWord(word) - } else { - accountViewModel.toast( - R.string.read_only_user, - R.string.login_with_a_private_key_to_be_able_to_show_word - ) - } + accountViewModel.toast( + R.string.read_only_user, + R.string.login_with_a_private_key_to_be_able_to_show_word + ) } else { accountViewModel.showWord(word) } @@ -320,14 +316,10 @@ fun MutedWordActionOptions( } else { HideWordButton { if (!accountViewModel.isWriteable()) { - if (accountViewModel.loggedInWithExternalSigner()) { - accountViewModel.hideWord(word) - } else { - accountViewModel.toast( - R.string.read_only_user, - R.string.login_with_a_private_key_to_be_able_to_hide_word - ) - } + accountViewModel.toast( + R.string.read_only_user, + R.string.login_with_a_private_key_to_be_able_to_hide_word + ) } else { accountViewModel.hideWord(word) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt index c93b5f0e0..def8f0859 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt @@ -15,7 +15,6 @@ import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.Immutable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -25,6 +24,7 @@ import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.res.stringResource import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.service.NostrHomeDataSource import com.vitorpamplona.amethyst.service.OnlineChecker @@ -216,10 +216,9 @@ fun WatchAccountForHomeScreen( repliesFeedViewModel: NostrHomeRepliesFeedViewModel, accountViewModel: AccountViewModel ) { - val accountState by accountViewModel.accountLiveData.observeAsState() - val followState by accountViewModel.account.userProfile().live().follows.observeAsState() + val homeFollowList by accountViewModel.account.liveHomeFollowLists.collectAsStateWithLifecycle() - LaunchedEffect(accountViewModel, accountState?.account?.defaultHomeFollowList, followState) { + LaunchedEffect(accountViewModel, homeFollowList) { NostrHomeDataSource.invalidateFilters() homeFeedViewModel.checkKeysInvalidateDataAndSendToTop() repliesFeedViewModel.checkKeysInvalidateDataAndSendToTop() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoadRedirectScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoadRedirectScreen.kt index 15b1b408c..c2ec320d1 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoadRedirectScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoadRedirectScreen.kt @@ -25,6 +25,8 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.navigation.Route import com.vitorpamplona.quartz.events.ChannelCreateEvent import com.vitorpamplona.quartz.events.ChatroomKeyable +import com.vitorpamplona.quartz.events.Event +import com.vitorpamplona.quartz.events.EventInterface import com.vitorpamplona.quartz.events.GiftWrapEvent import com.vitorpamplona.quartz.events.SealedGossipEvent import kotlinx.coroutines.Dispatchers @@ -75,36 +77,12 @@ fun LoadRedirectScreen(baseNote: Note, accountViewModel: AccountViewModel, nav: LaunchedEffect(key1 = noteState) { val note = noteState?.note ?: return@LaunchedEffect - var event = note.event - val channelHex = note.channelHex() + val event = note.event - if (event is GiftWrapEvent) { - event = accountViewModel.unwrap(event) - } - - if (event is SealedGossipEvent) { - event = accountViewModel.unseal(event) - } - - if (event == null) { - // stay here, loading - } else if (event is ChannelCreateEvent) { - nav("Channel/${note.idHex}") - } else if (event is ChatroomKeyable) { - note.author?.let { - val withKey = (event as ChatroomKeyable) - .chatroomKey(accountViewModel.userProfile().pubkeyHex) - - withContext(Dispatchers.IO) { - accountViewModel.userProfile().createChatroom(withKey) - } - - nav("Room/${withKey.hashCode()}") + if (event != null) { + withContext(Dispatchers.IO) { + redirect(event, note, accountViewModel, nav) } - } else if (channelHex != null) { - nav("Channel/$channelHex") - } else { - nav("Note/${note.idHex}") } } @@ -119,3 +97,36 @@ fun LoadRedirectScreen(baseNote: Note, accountViewModel: AccountViewModel, nav: Text(stringResource(R.string.looking_for_event, baseNote.idHex)) } } + +fun redirect(event: EventInterface, note: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) { + val channelHex = note.channelHex() + + if (event is GiftWrapEvent) { + accountViewModel.unwrap(event) { + redirect(it, note, accountViewModel, nav) + } + } else if (event is SealedGossipEvent) { + accountViewModel.unseal(event) { + redirect(it, note, accountViewModel, nav) + } + } else { + if (event == null) { + // stay here, loading + } else if (event is ChannelCreateEvent) { + nav("Channel/${note.idHex}") + } else if (event is ChatroomKeyable) { + note.author?.let { + val withKey = (event as ChatroomKeyable) + .chatroomKey(accountViewModel.userProfile().pubkeyHex) + + accountViewModel.userProfile().createChatroom(withKey) + + nav("Room/${withKey.hashCode()}") + } + } else if (channelHex != null) { + nav("Channel/$channelHex") + } else { + nav("Note/${note.idHex}") + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt index 740e34951..41e6cba07 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt @@ -453,9 +453,7 @@ fun FloatingButtons( } is AccountState.LoggedInViewOnly -> { - if (accountViewModel.loggedInWithExternalSigner()) { - WritePermissionButtons(navEntryState, accountViewModel, nav, navScrollToTop) - } + WritePermissionButtons(navEntryState, accountViewModel, nav, navScrollToTop) } is AccountState.LoggedOff -> { // Does nothing. diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt index f8851fa4d..d984ca7e9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt @@ -15,7 +15,6 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.Stable import androidx.compose.runtime.State import androidx.compose.runtime.getValue -import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -141,9 +140,9 @@ fun WatchAccountForNotifications( notifFeedViewModel: NotificationViewModel, accountViewModel: AccountViewModel ) { - val accountState by accountViewModel.accountLiveData.observeAsState() + val listState by accountViewModel.account.liveStoriesFollowLists.collectAsStateWithLifecycle() - LaunchedEffect(accountViewModel, accountState?.account?.defaultNotificationFollowList) { + LaunchedEffect(accountViewModel, listState) { NostrAccountDataSource.invalidateFilters() notifFeedViewModel.checkKeysInvalidateDataAndSendToTop() } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt index ca03b0100..b0d4a1c95 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt @@ -806,14 +806,10 @@ private fun DisplayFollowUnfollowButton( if (isLoggedInFollowingUser) { UnfollowButton { if (!accountViewModel.isWriteable()) { - if (accountViewModel.loggedInWithExternalSigner()) { - accountViewModel.unfollow(baseUser) - } else { - accountViewModel.toast( - R.string.read_only_user, - R.string.login_with_a_private_key_to_be_able_to_unfollow - ) - } + accountViewModel.toast( + R.string.read_only_user, + R.string.login_with_a_private_key_to_be_able_to_unfollow + ) } else { accountViewModel.unfollow(baseUser) } @@ -822,14 +818,10 @@ private fun DisplayFollowUnfollowButton( if (isUserFollowingLoggedIn) { FollowButton(R.string.follow_back) { if (!accountViewModel.isWriteable()) { - if (accountViewModel.loggedInWithExternalSigner()) { - accountViewModel.follow(baseUser) - } else { - accountViewModel.toast( - R.string.read_only_user, - R.string.login_with_a_private_key_to_be_able_to_follow - ) - } + accountViewModel.toast( + R.string.read_only_user, + R.string.login_with_a_private_key_to_be_able_to_follow + ) } else { accountViewModel.follow(baseUser) } @@ -837,14 +829,10 @@ private fun DisplayFollowUnfollowButton( } else { FollowButton(R.string.follow) { if (!accountViewModel.isWriteable()) { - if (accountViewModel.loggedInWithExternalSigner()) { - accountViewModel.follow(baseUser) - } else { - accountViewModel.toast( - R.string.read_only_user, - R.string.login_with_a_private_key_to_be_able_to_follow - ) - } + accountViewModel.toast( + R.string.read_only_user, + R.string.login_with_a_private_key_to_be_able_to_follow + ) } else { accountViewModel.follow(baseUser) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ReportNoteDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ReportNoteDialog.kt index 591bc3e28..faf184d92 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ReportNoteDialog.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ReportNoteDialog.kt @@ -27,7 +27,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -45,8 +44,6 @@ import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon import com.vitorpamplona.amethyst.ui.theme.WarningColor import com.vitorpamplona.quartz.events.ReportEvent import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -82,8 +79,6 @@ fun ReportNoteDialog(note: Note, accountViewModel: AccountViewModel, onDismiss: ) } ) { pad -> - val scope = rememberCoroutineScope() - Column( modifier = Modifier.padding(16.dp, pad.calculateTopPadding(), 16.dp, pad.calculateBottomPadding()), verticalArrangement = Arrangement.SpaceAround @@ -99,10 +94,8 @@ fun ReportNoteDialog(note: Note, accountViewModel: AccountViewModel, onDismiss: text = stringResource(R.string.report_dialog_block_hide_user_btn), icon = Icons.Default.Block, onClick = { - scope.launch(Dispatchers.IO) { - note.author?.let { accountViewModel.hide(it) } - onDismiss() - } + note.author?.let { accountViewModel.hide(it) } + onDismiss() } ) SpacerH16() @@ -138,15 +131,13 @@ fun ReportNoteDialog(note: Note, accountViewModel: AccountViewModel, onDismiss: icon = Icons.Default.Report, enabled = selectedReason in 0..reportTypes.lastIndex, onClick = { - scope.launch(Dispatchers.IO) { - accountViewModel.report( - note, - reportTypes[selectedReason].first, - additionalReason - ) - note.author?.let { accountViewModel.hide(it) } - onDismiss() - } + accountViewModel.report( + note, + reportTypes[selectedReason].first, + additionalReason + ) + note.author?.let { accountViewModel.hide(it) } + onDismiss() } ) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt index 9fc550f68..6c242e8c2 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt @@ -114,9 +114,9 @@ fun VideoScreen( @Composable fun WatchAccountForVideoScreen(videoFeedView: NostrVideoFeedViewModel, accountViewModel: AccountViewModel) { - val accountState by accountViewModel.accountLiveData.observeAsState() + val listState by accountViewModel.account.liveStoriesFollowLists.collectAsStateWithLifecycle() - LaunchedEffect(accountViewModel, accountState?.account?.defaultStoriesFollowList) { + LaunchedEffect(accountViewModel, listState) { NostrVideoDataSource.resetFilters() videoFeedView.checkKeysInvalidateDataAndSendToTop() } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt index 0f243ff39..26a7afadd 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt @@ -1,6 +1,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedOff import android.app.Activity +import android.util.Log import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts @@ -32,6 +33,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -68,16 +70,17 @@ import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.ServiceManager -import com.vitorpamplona.amethyst.service.ExternalSignerUtils import com.vitorpamplona.amethyst.service.PackageUtils -import com.vitorpamplona.amethyst.service.SignerType +import com.vitorpamplona.amethyst.ui.MainActivity +import com.vitorpamplona.amethyst.ui.components.getActivity import com.vitorpamplona.amethyst.ui.qrcode.SimpleQrCodeScanner import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.ConnectOrbotDialog import com.vitorpamplona.amethyst.ui.theme.Font14SP import com.vitorpamplona.amethyst.ui.theme.Size35dp import com.vitorpamplona.amethyst.ui.theme.placeholderText +import com.vitorpamplona.quartz.signers.ExternalSignerLauncher +import com.vitorpamplona.quartz.signers.SignerType import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -103,24 +106,62 @@ fun LoginPage( val scope = rememberCoroutineScope() var loginWithExternalSigner by remember { mutableStateOf(false) } - val activity = rememberLauncherForActivityResult( - contract = ActivityResultContracts.StartActivityForResult(), - onResult = { - loginWithExternalSigner = false - ExternalSignerUtils.isActivityRunning = false - ServiceManager.shouldPauseService = true - if (it.resultCode != Activity.RESULT_OK) { - scope.launch(Dispatchers.Main) { - Toast.makeText( - Amethyst.instance, - "Sign request rejected", - Toast.LENGTH_SHORT - ).show() + if (loginWithExternalSigner) { + val externalSignerLauncher = remember { ExternalSignerLauncher("") } + + val launcher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartActivityForResult(), + onResult = { result -> + if (result.resultCode != Activity.RESULT_OK) { + scope.launch(Dispatchers.Main) { + Toast.makeText( + Amethyst.instance, + "Sign request rejected", + Toast.LENGTH_SHORT + ).show() + } + } else { + result.data?.let { + externalSignerLauncher.newResult(it) + } } - return@rememberLauncherForActivityResult - } else { - val event = it.data?.getStringExtra("signature") ?: "" - key.value = TextFieldValue(event) + } + ) + + val activity = getActivity() as MainActivity + + DisposableEffect(launcher, activity, externalSignerLauncher) { + externalSignerLauncher.registerLauncher( + launcher = { + try { + activity.prepareToLaunchSigner() + launcher.launch(it) + } catch (e: Exception) { + Log.e("Signer", "Error opening Signer app", e) + scope.launch(Dispatchers.Main) { + Toast.makeText( + Amethyst.instance, + R.string.error_opening_external_signer, + Toast.LENGTH_SHORT + ).show() + } + } + }, + contentResolver = { Amethyst.instance.contentResolver } + ) + onDispose { + externalSignerLauncher.clearLauncher() + } + } + + LaunchedEffect(loginWithExternalSigner, externalSignerLauncher) { + externalSignerLauncher.openSignerApp( + "", + SignerType.GET_PUBLIC_KEY, + "", + "" + ) { pubkey -> + key.value = TextFieldValue(pubkey) if (!acceptedTerms.value) { termsAcceptanceIsRequired = context.getString(R.string.acceptance_of_terms_is_required) @@ -137,18 +178,6 @@ fun LoginPage( } } } - ) - - LaunchedEffect(loginWithExternalSigner) { - if (loginWithExternalSigner) { - ExternalSignerUtils.openSigner( - "", - SignerType.GET_PUBLIC_KEY, - activity, - "", - "" - ) - } } Column( @@ -400,7 +429,12 @@ fun LoginPage( return@Button } - val result = ExternalSignerUtils.getDataFromResolver(SignerType.GET_PUBLIC_KEY, arrayOf("login")) + val result = ExternalSignerLauncher("").getDataFromResolver( + SignerType.GET_PUBLIC_KEY, + arrayOf("login"), + contentResolver = Amethyst.instance.contentResolver + ) + if (result == null) { loginWithExternalSigner = true return@Button diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fec267a4f..3901f92ca 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -601,7 +601,9 @@ Paid Wallet %1$s Error opening signer app - Sign request rejected + The signer app could not be found. Check if the app hasn\'t been uninstalled + Signer Application Rejected + Make sure the signer application has authorized this transaction No Wallets found to pay a lightning invoice (Error: %1$s). Please install a Lightning wallet to use zaps No Wallets found to pay a lightning invoice. Please install a Lightning wallet to use zaps diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/GiftWrapReceivingBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/GiftWrapReceivingBenchmark.kt index a84cbd84d..71102888b 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/GiftWrapReceivingBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/GiftWrapReceivingBenchmark.kt @@ -13,6 +13,7 @@ import com.vitorpamplona.quartz.events.Event import com.vitorpamplona.quartz.events.GiftWrapEvent import com.vitorpamplona.quartz.events.Gossip import com.vitorpamplona.quartz.events.SealedGossipEvent +import com.vitorpamplona.quartz.signers.NostrSignerInternal import junit.framework.TestCase.assertNotNull import org.junit.Rule import org.junit.Test @@ -43,7 +44,7 @@ class GiftWrapReceivingBenchmark { markAsSensitive = true, zapRaiserAmount = 10000, geohash = null, - keyPair = sender + signer = NostrSignerInternal(keyPair = sender) ) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/AdvertisedRelayListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/AdvertisedRelayListEvent.kt index 2f256ee16..938bf0f57 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/AdvertisedRelayListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/AdvertisedRelayListEvent.kt @@ -6,6 +6,7 @@ import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.encoders.ATag import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner @Immutable class AdvertisedRelayListEvent( @@ -40,9 +41,10 @@ class AdvertisedRelayListEvent( fun create( list: List, - privateKey: ByteArray, - createdAt: Long = TimeUtils.now() - ): AdvertisedRelayListEvent { + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (AdvertisedRelayListEvent) -> Unit + ) { val tags = list.map { if (it.type == AdvertisedRelayType.BOTH) { listOf(it.relayUrl) @@ -51,10 +53,8 @@ class AdvertisedRelayListEvent( } } val msg = "" - val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey() - val id = generateId(pubKey, createdAt, kind, tags, msg) - val sig = CryptoUtils.sign(id, privateKey) - return AdvertisedRelayListEvent(id.toHexKey(), pubKey, createdAt, tags, msg, sig.toHexKey()) + + signer.sign(createdAt, kind, tags, msg, onReady) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/AppDefinitionEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/AppDefinitionEvent.kt index 04108eb10..d3c4239d5 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/AppDefinitionEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/AppDefinitionEvent.kt @@ -4,6 +4,8 @@ import android.util.Log import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.encoders.ATag import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner +import com.vitorpamplona.quartz.utils.TimeUtils import java.io.ByteArrayInputStream @Immutable @@ -46,5 +48,14 @@ class AppDefinitionEvent( companion object { const val kind = 31990 + + fun create( + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (AppDefinitionEvent) -> Unit + ) { + val tags = mutableListOf>() + signer.sign(createdAt, kind, tags, "", onReady) + } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/AppRecommendationEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/AppRecommendationEvent.kt index d02a72640..6b8ce21d7 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/AppRecommendationEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/AppRecommendationEvent.kt @@ -3,6 +3,8 @@ package com.vitorpamplona.quartz.events import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.encoders.ATag import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner +import com.vitorpamplona.quartz.utils.TimeUtils @Immutable class AppRecommendationEvent( @@ -19,5 +21,14 @@ class AppRecommendationEvent( companion object { const val kind = 31989 + + fun create( + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (AppRecommendationEvent) -> Unit + ) { + val tags = mutableListOf>() + signer.sign(createdAt, kind, tags, "", onReady) + } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/AudioHeaderEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/AudioHeaderEvent.kt index a9ea96d6f..cdcc2fe39 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/AudioHeaderEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/AudioHeaderEvent.kt @@ -3,9 +3,8 @@ package com.vitorpamplona.quartz.events import androidx.compose.runtime.Immutable import com.fasterxml.jackson.module.kotlin.readValue import com.vitorpamplona.quartz.utils.TimeUtils -import com.vitorpamplona.quartz.encoders.toHexKey -import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner @Immutable class AudioHeaderEvent( @@ -30,15 +29,16 @@ class AudioHeaderEvent( private const val STREAM_URL = "stream_url" private const val WAVEFORM = "waveform" - fun create( + suspend fun create( description: String, downloadUrl: String, streamUrl: String? = null, wavefront: String? = null, sensitiveContent: Boolean? = null, - privateKey: ByteArray, - createdAt: Long = TimeUtils.now() - ): AudioHeaderEvent { + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (AudioHeaderEvent) -> Unit + ) { val tags = listOfNotNull( downloadUrl.let { listOf(DOWNLOAD_URL, it) }, streamUrl?.let { listOf(STREAM_URL, it) }, @@ -52,11 +52,7 @@ class AudioHeaderEvent( } ) - val content = description - val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey() - val id = generateId(pubKey, createdAt, kind, tags, content) - val sig = CryptoUtils.sign(id, privateKey) - return AudioHeaderEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) + signer.sign(createdAt, kind, tags, description, onReady) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/AudioTrackEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/AudioTrackEvent.kt index 3c1784015..f0cbd176e 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/AudioTrackEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/AudioTrackEvent.kt @@ -6,6 +6,7 @@ import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.encoders.ATag import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner @Immutable class AudioTrackEvent( @@ -40,9 +41,10 @@ class AudioTrackEvent( price: String? = null, cover: String? = null, subject: String? = null, - privateKey: ByteArray, - createdAt: Long = TimeUtils.now() - ): AudioTrackEvent { + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (AudioTrackEvent) -> Unit + ) { val tags = listOfNotNull( listOf(MEDIA, media), listOf(TYPE, type), @@ -51,10 +53,7 @@ class AudioTrackEvent( subject?.let { listOf(SUBJECT, it) } ) - val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey() - val id = generateId(pubKey, createdAt, kind, tags, "") - val sig = CryptoUtils.sign(id, privateKey) - return AudioTrackEvent(id.toHexKey(), pubKey, createdAt, tags, "", sig.toHexKey()) + signer.sign(createdAt, kind, tags, "", onReady) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/BookmarkListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/BookmarkListEvent.kt index 9c93601f3..3266471fc 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/BookmarkListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/BookmarkListEvent.kt @@ -6,6 +6,7 @@ import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.encoders.ATag import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner @Immutable class BookmarkListEvent( @@ -16,36 +17,156 @@ class BookmarkListEvent( content: String, sig: HexKey ) : GeneralListEvent(id, pubKey, createdAt, kind, tags, content, sig) { - @Transient - var decryptedContent = "" - companion object { const val kind = 30001 + fun addEvent( + earlierVersion: BookmarkListEvent?, + eventId: HexKey, + isPrivate: Boolean, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (BookmarkListEvent) -> Unit + ) = addTag(earlierVersion, "e", eventId, isPrivate, signer, createdAt, onReady) + + fun addReplaceable( + earlierVersion: BookmarkListEvent?, + aTag: ATag, + isPrivate: Boolean, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (BookmarkListEvent) -> Unit + ) = addTag(earlierVersion, "a", aTag.toTag(), isPrivate, signer, createdAt, onReady) + + fun addTag( + earlierVersion: BookmarkListEvent?, + tagName: String, + tagValue: HexKey, + isPrivate: Boolean, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (BookmarkListEvent) -> Unit + ) { + add( + earlierVersion, + listOf(listOf(tagName, tagValue)), + isPrivate, + signer, + createdAt, + onReady + ) + } + + fun add( + earlierVersion: BookmarkListEvent?, + listNewTags: List>, + isPrivate: Boolean, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (BookmarkListEvent) -> Unit + ) { + if (isPrivate) { + if (earlierVersion != null) { + earlierVersion.privateTagsOrEmpty(signer) { privateTags -> + encryptTags( + privateTags = privateTags.plus(listNewTags), + signer = signer + ) { encryptedTags -> + create( + content = encryptedTags, + tags = earlierVersion.tags, + signer = signer, + createdAt = createdAt, + onReady = onReady + ) + } + } + } else { + encryptTags( + privateTags = listNewTags, + signer = signer + ) { encryptedTags -> + create( + content = encryptedTags, + tags = emptyList(), + signer = signer, + createdAt = createdAt, + onReady = onReady + ) + } + } + } else { + create( + content = earlierVersion?.content ?: "", + tags = (earlierVersion?.tags ?: emptyList()).plus(listNewTags), + signer = signer, + createdAt = createdAt, + onReady = onReady + ) + } + } + + fun removeEvent( + earlierVersion: BookmarkListEvent, + eventId: HexKey, + isPrivate: Boolean, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (BookmarkListEvent) -> Unit + ) = removeTag(earlierVersion, "e", eventId, isPrivate, signer, createdAt, onReady) + + fun removeReplaceable( + earlierVersion: BookmarkListEvent, + aTag: ATag, + isPrivate: Boolean, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (BookmarkListEvent) -> Unit + ) = removeTag(earlierVersion, "a", aTag.toTag(), isPrivate, signer, createdAt, onReady) + + private fun removeTag( + earlierVersion: BookmarkListEvent, + tagName: String, + tagValue: HexKey, + isPrivate: Boolean, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (BookmarkListEvent) -> Unit + ) { + if (isPrivate) { + earlierVersion.privateTagsOrEmpty(signer) { privateTags -> + encryptTags( + privateTags = privateTags.filter { it.size <= 1 || !(it[0] == tagName && it[1] == tagValue) }, + signer = signer + ) { encryptedTags -> + create( + content = encryptedTags, + tags = earlierVersion.tags.filter { it.size <= 1 || !(it[0] == tagName && it[1] == tagValue) }, + signer = signer, + createdAt = createdAt, + onReady = onReady + ) + } + } + } else { + create( + content = earlierVersion.content, + tags = earlierVersion.tags.filter { it.size <= 1 || !(it[0] == tagName && it[1] == tagValue) }, + signer = signer, + createdAt = createdAt, + onReady = onReady + ) + } + } + fun create( - name: String = "", - events: List? = null, - users: List? = null, - addresses: List? = null, content: String, - pubKey: HexKey, - createdAt: Long = TimeUtils.now() - ): BookmarkListEvent { - val tags = mutableListOf>() - tags.add(listOf("d", name)) - - events?.forEach { - tags.add(listOf("e", it)) - } - users?.forEach { - tags.add(listOf("p", it)) - } - addresses?.forEach { - tags.add(listOf("a", it.toTag())) - } - - val id = generateId(pubKey, createdAt, kind, tags, content) - return BookmarkListEvent(id.toHexKey(), pubKey, createdAt, tags, content, "") + tags: List>, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (BookmarkListEvent) -> Unit + ) { + signer.sign(createdAt, kind, tags, content, onReady) } fun create( @@ -59,12 +180,10 @@ class BookmarkListEvent( privUsers: List? = null, privAddresses: List? = null, - privateKey: ByteArray, - createdAt: Long = TimeUtils.now() - ): BookmarkListEvent { - val pubKey = CryptoUtils.pubkeyCreate(privateKey) - val content = createPrivateTags(privEvents, privUsers, privAddresses, privateKey, pubKey) - + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (BookmarkListEvent) -> Unit + ) { val tags = mutableListOf>() tags.add(listOf("d", name)) @@ -78,15 +197,9 @@ class BookmarkListEvent( tags.add(listOf("a", it.toTag())) } - val id = generateId(pubKey.toHexKey(), createdAt, kind, tags, content) - val sig = CryptoUtils.sign(id, privateKey) - return BookmarkListEvent(id.toHexKey(), pubKey.toHexKey(), createdAt, tags, content, sig.toHexKey()) - } - - fun create( - unsignedEvent: BookmarkListEvent, signature: String - ): BookmarkListEvent { - return BookmarkListEvent(unsignedEvent.id, unsignedEvent.pubKey, unsignedEvent.createdAt, unsignedEvent.tags, unsignedEvent.content, signature) + createPrivateTags(privEvents, privUsers, privAddresses, signer) { content -> + signer.sign(createdAt, kind, tags, content, onReady) + } } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/CalendarDateSlotEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/CalendarDateSlotEvent.kt index a9b234436..a041f45c5 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/CalendarDateSlotEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/CalendarDateSlotEvent.kt @@ -6,6 +6,7 @@ import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.encoders.ATag import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner @Immutable class CalendarDateSlotEvent( @@ -28,14 +29,12 @@ class CalendarDateSlotEvent( const val kind = 31922 fun create( - privateKey: ByteArray, - createdAt: Long = TimeUtils.now() - ): CalendarDateSlotEvent { + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (CalendarDateSlotEvent) -> Unit + ) { val tags = mutableListOf>() - val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey() - val id = generateId(pubKey, createdAt, kind, tags, "") - val sig = CryptoUtils.sign(id, privateKey) - return CalendarDateSlotEvent(id.toHexKey(), pubKey, createdAt, tags, "", sig.toHexKey()) + signer.sign(createdAt, kind, tags, "", onReady) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/CalendarEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/CalendarEvent.kt index 9005e5eb5..605800e87 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/CalendarEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/CalendarEvent.kt @@ -6,6 +6,7 @@ import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.encoders.ATag import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner @Immutable class CalendarEvent( @@ -20,14 +21,12 @@ class CalendarEvent( const val kind = 31924 fun create( - privateKey: ByteArray, - createdAt: Long = TimeUtils.now() - ): CalendarEvent { + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (CalendarEvent) -> Unit + ) { val tags = mutableListOf>() - val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey() - val id = generateId(pubKey, createdAt, kind, tags, "") - val sig = CryptoUtils.sign(id, privateKey) - return CalendarEvent(id.toHexKey(), pubKey, createdAt, tags, "", sig.toHexKey()) + signer.sign(createdAt, kind, tags, "", onReady) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/CalendarRSVPEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/CalendarRSVPEvent.kt index 282104501..495d1ff82 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/CalendarRSVPEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/CalendarRSVPEvent.kt @@ -6,6 +6,7 @@ import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.encoders.ATag import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner @Immutable class CalendarRSVPEvent( @@ -30,14 +31,12 @@ class CalendarRSVPEvent( const val kind = 31925 fun create( - privateKey: ByteArray, - createdAt: Long = TimeUtils.now() - ): CalendarRSVPEvent { + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (CalendarRSVPEvent) -> Unit + ) { val tags = mutableListOf>() - val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey() - val id = generateId(pubKey, createdAt, kind, tags, "") - val sig = CryptoUtils.sign(id, privateKey) - return CalendarRSVPEvent(id.toHexKey(), pubKey, createdAt, tags, "", sig.toHexKey()) + signer.sign(createdAt, kind, tags, "", onReady) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/CalendarTimeSlotEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/CalendarTimeSlotEvent.kt index d0bc33693..ad7423be8 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/CalendarTimeSlotEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/CalendarTimeSlotEvent.kt @@ -6,6 +6,7 @@ import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.encoders.ATag import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner @Immutable class CalendarTimeSlotEvent( @@ -32,14 +33,12 @@ class CalendarTimeSlotEvent( const val kind = 31923 fun create( - privateKey: ByteArray, - createdAt: Long = TimeUtils.now() - ): CalendarTimeSlotEvent { + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (CalendarTimeSlotEvent) -> Unit + ) { val tags = mutableListOf>() - val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey() - val id = generateId(pubKey, createdAt, kind, tags, "") - val sig = CryptoUtils.sign(id, privateKey) - return CalendarTimeSlotEvent(id.toHexKey(), pubKey, createdAt, tags, "", sig.toHexKey()) + signer.sign(createdAt, kind, tags, "", onReady) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/ChannelCreateEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/ChannelCreateEvent.kt index 28e43f9f2..d5220c4b3 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/ChannelCreateEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/ChannelCreateEvent.kt @@ -9,6 +9,7 @@ import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.crypto.KeyPair import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner @Immutable class ChannelCreateEvent( @@ -29,7 +30,30 @@ class ChannelCreateEvent( companion object { const val kind = 40 - fun create(channelInfo: ChannelData?, keyPair: KeyPair, createdAt: Long = TimeUtils.now()): ChannelCreateEvent { + fun create( + name: String?, + about: String?, + picture: String?, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (ChannelCreateEvent) -> Unit + ) { + return 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) { mapper.writeValueAsString(channelInfo) @@ -41,15 +65,7 @@ class ChannelCreateEvent( "" } - val pubKey = keyPair.pubKey.toHexKey() - val tags = emptyList>() - val id = generateId(pubKey, createdAt, kind, tags, content) - val sig = if (keyPair.privKey == null) null else CryptoUtils.sign(id, keyPair.pubKey) - return ChannelCreateEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig?.toHexKey() ?: "") - } - - fun create(unsignedEvent: ChannelCreateEvent, signature: String): ChannelCreateEvent { - return ChannelCreateEvent(unsignedEvent.id, unsignedEvent.pubKey, unsignedEvent.createdAt, unsignedEvent.tags, unsignedEvent.content, signature) + signer.sign(createdAt, kind, emptyList(), content, onReady) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/ChannelHideMessageEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/ChannelHideMessageEvent.kt index 7e8b24136..6650e9ad6 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/ChannelHideMessageEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/ChannelHideMessageEvent.kt @@ -5,6 +5,7 @@ import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner @Immutable class ChannelHideMessageEvent( @@ -26,16 +27,19 @@ class ChannelHideMessageEvent( companion object { const val kind = 43 - fun create(reason: String, messagesToHide: List?, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ChannelHideMessageEvent { - val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey() + fun create( + reason: String, + messagesToHide: List?, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (ChannelHideMessageEvent) -> Unit + ) { val tags = messagesToHide?.map { listOf("e", it) } ?: emptyList() - val id = generateId(pubKey, createdAt, kind, tags, reason) - val sig = CryptoUtils.sign(id, privateKey) - return ChannelHideMessageEvent(id.toHexKey(), pubKey, createdAt, tags, reason, sig.toHexKey()) + signer.sign(createdAt, kind, tags, reason, onReady) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/ChannelMessageEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/ChannelMessageEvent.kt index 1b7fb60a8..239b8c5f2 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/ChannelMessageEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/ChannelMessageEvent.kt @@ -6,6 +6,7 @@ import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.crypto.KeyPair import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner @Immutable class ChannelMessageEvent( @@ -34,12 +35,13 @@ class ChannelMessageEvent( replyTos: List? = null, mentions: List? = null, zapReceiver: List? = null, - keyPair: KeyPair, + signer: NostrSigner, createdAt: Long = TimeUtils.now(), markAsSensitive: Boolean, zapRaiserAmount: Long?, - geohash: String? = null - ): ChannelMessageEvent { + geohash: String? = null, + onReady: (ChannelMessageEvent) -> Unit + ) { val content = message val tags = mutableListOf( listOf("e", channel, "", "root") @@ -63,16 +65,7 @@ class ChannelMessageEvent( tags.add(listOf("g", it)) } - val pubKey = keyPair.pubKey.toHexKey() - val id = generateId(pubKey, createdAt, kind, tags, content) - val sig = if (keyPair.privKey == null) null else CryptoUtils.sign(id, keyPair.privKey) - return ChannelMessageEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig?.toHexKey() ?: "") - } - - fun create( - unsignedEvent: ChannelMessageEvent, signature: String - ): ChannelMessageEvent { - return ChannelMessageEvent(unsignedEvent.id, unsignedEvent.pubKey, unsignedEvent.createdAt, unsignedEvent.tags, unsignedEvent.content, signature) + signer.sign(createdAt, kind, tags, content, onReady) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/ChannelMetadataEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/ChannelMetadataEvent.kt index 245397b59..f2439c8a2 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/ChannelMetadataEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/ChannelMetadataEvent.kt @@ -7,6 +7,7 @@ import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.crypto.KeyPair import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner @Immutable class ChannelMetadataEvent( @@ -30,7 +31,33 @@ class ChannelMetadataEvent( companion object { const val kind = 41 - fun create(newChannelInfo: ChannelCreateEvent.ChannelData?, originalChannelIdHex: String, keyPair: KeyPair, createdAt: Long = TimeUtils.now()): ChannelMetadataEvent { + 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) { mapper.writeValueAsString(newChannelInfo) @@ -38,15 +65,8 @@ class ChannelMetadataEvent( "" } - val pubKey = keyPair.pubKey.toHexKey() val tags = listOf(listOf("e", originalChannelIdHex, "", "root")) - val id = generateId(pubKey, createdAt, kind, tags, content) - val sig = if (keyPair.privKey == null) null else CryptoUtils.sign(id, keyPair.privKey) - return ChannelMetadataEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig?.toHexKey() ?: "") - } - - fun create(unsignedEvent: ChannelMetadataEvent, signature: String): ChannelMetadataEvent { - return ChannelMetadataEvent(unsignedEvent.id, unsignedEvent.pubKey, unsignedEvent.createdAt, unsignedEvent.tags, unsignedEvent.content, signature) + signer.sign(createdAt, kind, tags, content, onReady) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/ChannelMuteUserEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/ChannelMuteUserEvent.kt index 74702aa28..957cb7bbf 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/ChannelMuteUserEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/ChannelMuteUserEvent.kt @@ -5,6 +5,7 @@ import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner @Immutable class ChannelMuteUserEvent( @@ -26,17 +27,19 @@ class ChannelMuteUserEvent( companion object { const val kind = 44 - fun create(reason: String, usersToMute: List?, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ChannelMuteUserEvent { + fun create( + reason: String, + usersToMute: List?, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (ChannelMuteUserEvent) -> Unit + ) { val content = reason - val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey() - val tags = - usersToMute?.map { - listOf("p", it) - } ?: emptyList() + val tags = usersToMute?.map { + listOf("p", it) + } ?: emptyList() - val id = generateId(pubKey, createdAt, kind, tags, content) - val sig = CryptoUtils.sign(id, privateKey) - return ChannelMuteUserEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) + signer.sign(createdAt, kind, tags, content, onReady) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/ChatMessageEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/ChatMessageEvent.kt index 0918e7450..59e83f895 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/ChatMessageEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/ChatMessageEvent.kt @@ -3,10 +3,8 @@ package com.vitorpamplona.quartz.events import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import com.vitorpamplona.quartz.utils.TimeUtils -import com.vitorpamplona.quartz.encoders.toHexKey -import com.vitorpamplona.quartz.crypto.CryptoUtils -import com.vitorpamplona.quartz.crypto.KeyPair import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.toImmutableSet @@ -62,10 +60,10 @@ class ChatMessageEvent( markAsSensitive: Boolean = false, zapRaiserAmount: Long? = null, geohash: String? = null, - keyPair: KeyPair, - createdAt: Long = TimeUtils.now() - ): ChatMessageEvent { - val content = msg + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (ChatMessageEvent) -> Unit + ) { val tags = mutableListOf>() to?.forEach { tags.add(listOf("p", it)) @@ -92,17 +90,7 @@ class ChatMessageEvent( tags.add(listOf("subject", it)) } - val pubKey = keyPair.pubKey.toHexKey() - val id = generateId(pubKey, createdAt, ClassifiedsEvent.kind, tags, content) - val sig = if (keyPair.privKey == null) null else CryptoUtils.sign(id, keyPair.privKey) - return ChatMessageEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig?.toHexKey() ?: "") - } - - fun create( - unsignedEvent: ChatMessageEvent, - signature: String - ): ChatMessageEvent { - return ChatMessageEvent(unsignedEvent.id, unsignedEvent.pubKey, unsignedEvent.createdAt, unsignedEvent.tags, unsignedEvent.content, signature) + signer.sign(createdAt, kind, tags, msg, onReady) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/ClassifiedsEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/ClassifiedsEvent.kt index e22b0e0cc..021d928cc 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/ClassifiedsEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/ClassifiedsEvent.kt @@ -6,6 +6,7 @@ import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.encoders.ATag import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner @Immutable class ClassifiedsEvent( @@ -41,9 +42,10 @@ class ClassifiedsEvent( price: Price?, location: String?, publishedAt: Long?, - privateKey: ByteArray, - createdAt: Long = TimeUtils.now() - ): ClassifiedsEvent { + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (ClassifiedsEvent) -> Unit + ) { val tags = mutableListOf>() tags.add(listOf("d", dTag)) @@ -63,10 +65,7 @@ class ClassifiedsEvent( publishedAt?.let { tags.add(listOf("publishedAt", it.toString())) } title?.let { tags.add(listOf("title", it)) } - val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey() - val id = generateId(pubKey, createdAt, kind, tags, "") - val sig = CryptoUtils.sign(id, privateKey) - return ClassifiedsEvent(id.toHexKey(), pubKey, createdAt, tags, "", sig.toHexKey()) + signer.sign(createdAt, kind, tags, "", onReady) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/CommunityDefinitionEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/CommunityDefinitionEvent.kt index 447261d01..769712e41 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/CommunityDefinitionEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/CommunityDefinitionEvent.kt @@ -6,6 +6,7 @@ import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.encoders.ATag import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner @Immutable class CommunityDefinitionEvent( @@ -26,14 +27,12 @@ class CommunityDefinitionEvent( const val kind = 34550 fun create( - privateKey: ByteArray, - createdAt: Long = TimeUtils.now() - ): CommunityDefinitionEvent { + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (CommunityDefinitionEvent) -> Unit + ) { val tags = mutableListOf>() - val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey() - val id = generateId(pubKey, createdAt, kind, tags, "") - val sig = CryptoUtils.sign(id, privateKey) - return CommunityDefinitionEvent(id.toHexKey(), pubKey, createdAt, tags, "", sig.toHexKey()) + signer.sign(createdAt, kind, tags, "", onReady) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/CommunityPostApprovalEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/CommunityPostApprovalEvent.kt index 8089e8bfa..7d9065ec5 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/CommunityPostApprovalEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/CommunityPostApprovalEvent.kt @@ -7,6 +7,7 @@ import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.encoders.ATag import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner @Immutable class CommunityPostApprovalEvent( @@ -45,19 +46,23 @@ class CommunityPostApprovalEvent( companion object { const val kind = 4550 - fun create(approvedPost: Event, community: CommunityDefinitionEvent, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): GenericRepostEvent { + fun create( + approvedPost: Event, + community: CommunityDefinitionEvent, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (CommunityPostApprovalEvent) -> Unit + ) { val content = approvedPost.toJson() val communities = listOf("a", community.address().toTag()) val replyToPost = listOf("e", approvedPost.id()) val replyToAuthor = listOf("p", approvedPost.pubKey()) - val kind = listOf("k", "${approvedPost.kind()}") + val innerKind = listOf("k", "${approvedPost.kind()}") - val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey() - val tags: List> = listOf(communities, replyToPost, replyToAuthor, kind) - val id = generateId(pubKey, createdAt, GenericRepostEvent.kind, tags, content) - val sig = CryptoUtils.sign(id, privateKey) - return GenericRepostEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) + val tags: List> = listOf(communities, replyToPost, replyToAuthor, innerKind) + + signer.sign(createdAt, kind, tags, content, onReady) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/ContactListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/ContactListEvent.kt index ace1b5f67..a35de2491 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/ContactListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/ContactListEvent.kt @@ -11,6 +11,7 @@ import com.vitorpamplona.quartz.crypto.KeyPair import com.vitorpamplona.quartz.encoders.ATag import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.decodePublicKey +import com.vitorpamplona.quartz.signers.NostrSigner @Immutable data class Contact(val pubKeyHex: String, val relayUri: String?) @@ -99,9 +100,10 @@ class ContactListEvent( followCommunities: List, followEvents: List, relayUse: Map?, - keyPair: KeyPair, - createdAt: Long = TimeUtils.now() - ): ContactListEvent { + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (ContactListEvent) -> Unit + ) { val content = if (relayUse != null) { mapper.writeValueAsString(relayUse) } else { @@ -131,122 +133,133 @@ class ContactListEvent( return create( content = content, tags = tags, - keyPair = keyPair, - createdAt = createdAt + signer = signer, + createdAt = createdAt, + onReady = onReady ) } - fun followUser(earlierVersion: ContactListEvent, pubKeyHex: String, keyPair: KeyPair, createdAt: Long = TimeUtils.now()): ContactListEvent { - if (earlierVersion.isTaggedUser(pubKeyHex)) return earlierVersion + fun followUser(earlierVersion: ContactListEvent, pubKeyHex: String, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (ContactListEvent) -> Unit) { + if (earlierVersion.isTaggedUser(pubKeyHex)) return return create( content = earlierVersion.content, tags = earlierVersion.tags.plus(element = listOf("p", pubKeyHex)), - keyPair = keyPair, - createdAt = createdAt + signer = signer, + createdAt = createdAt, + onReady = onReady ) } - fun unfollowUser(earlierVersion: ContactListEvent, pubKeyHex: String, keyPair: KeyPair, createdAt: Long = TimeUtils.now()): ContactListEvent { - if (!earlierVersion.isTaggedUser(pubKeyHex)) return earlierVersion + fun unfollowUser(earlierVersion: ContactListEvent, pubKeyHex: String, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (ContactListEvent) -> Unit) { + if (!earlierVersion.isTaggedUser(pubKeyHex)) return return create( content = earlierVersion.content, tags = earlierVersion.tags.filter { it.size > 1 && it[1] != pubKeyHex }, - keyPair = keyPair, - createdAt = createdAt + signer = signer, + createdAt = createdAt, + onReady = onReady ) } - fun followHashtag(earlierVersion: ContactListEvent, hashtag: String, keyPair: KeyPair, createdAt: Long = TimeUtils.now()): ContactListEvent { - if (earlierVersion.isTaggedHash(hashtag)) return earlierVersion + fun followHashtag(earlierVersion: ContactListEvent, hashtag: String, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (ContactListEvent) -> Unit) { + if (earlierVersion.isTaggedHash(hashtag)) return return create( content = earlierVersion.content, tags = earlierVersion.tags.plus(element = listOf("t", hashtag)), - keyPair = keyPair, - createdAt = createdAt + signer = signer, + createdAt = createdAt, + onReady = onReady ) } - fun unfollowHashtag(earlierVersion: ContactListEvent, hashtag: String, keyPair: KeyPair, createdAt: Long = TimeUtils.now()): ContactListEvent { - if (!earlierVersion.isTaggedHash(hashtag)) return earlierVersion + fun unfollowHashtag(earlierVersion: ContactListEvent, hashtag: String, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (ContactListEvent) -> Unit) { + if (!earlierVersion.isTaggedHash(hashtag)) return return create( content = earlierVersion.content, tags = earlierVersion.tags.filter { it.size > 1 && !it[1].equals(hashtag, true) }, - keyPair = keyPair, - createdAt = createdAt + signer = signer, + createdAt = createdAt, + onReady = onReady ) } - fun followGeohash(earlierVersion: ContactListEvent, hashtag: String, keyPair: KeyPair, createdAt: Long = TimeUtils.now()): ContactListEvent { - if (earlierVersion.isTaggedGeoHash(hashtag)) return earlierVersion + fun followGeohash(earlierVersion: ContactListEvent, hashtag: String, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (ContactListEvent) -> Unit) { + if (earlierVersion.isTaggedGeoHash(hashtag)) return return create( content = earlierVersion.content, tags = earlierVersion.tags.plus(element = listOf("g", hashtag)), - keyPair = keyPair, - createdAt = createdAt + signer = signer, + createdAt = createdAt, + onReady = onReady ) } - fun unfollowGeohash(earlierVersion: ContactListEvent, hashtag: String, keyPair: KeyPair, createdAt: Long = TimeUtils.now()): ContactListEvent { - if (!earlierVersion.isTaggedGeoHash(hashtag)) return earlierVersion + fun unfollowGeohash(earlierVersion: ContactListEvent, hashtag: String, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (ContactListEvent) -> Unit) { + if (!earlierVersion.isTaggedGeoHash(hashtag)) return return create( content = earlierVersion.content, tags = earlierVersion.tags.filter { it.size > 1 && it[1] != hashtag }, - keyPair = keyPair, - createdAt = createdAt + signer = signer, + createdAt = createdAt, + onReady = onReady ) } - fun followEvent(earlierVersion: ContactListEvent, idHex: String, keyPair: KeyPair, createdAt: Long = TimeUtils.now()): ContactListEvent { - if (earlierVersion.isTaggedEvent(idHex)) return earlierVersion + fun followEvent(earlierVersion: ContactListEvent, idHex: String, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (ContactListEvent) -> Unit) { + if (earlierVersion.isTaggedEvent(idHex)) return return create( content = earlierVersion.content, tags = earlierVersion.tags.plus(element = listOf("e", idHex)), - keyPair = keyPair, - createdAt = createdAt + signer = signer, + createdAt = createdAt, + onReady = onReady ) } - fun unfollowEvent(earlierVersion: ContactListEvent, idHex: String, keyPair: KeyPair, createdAt: Long = TimeUtils.now()): ContactListEvent { - if (!earlierVersion.isTaggedEvent(idHex)) return earlierVersion + fun unfollowEvent(earlierVersion: ContactListEvent, idHex: String, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (ContactListEvent) -> Unit) { + if (!earlierVersion.isTaggedEvent(idHex)) return return create( content = earlierVersion.content, tags = earlierVersion.tags.filter { it.size > 1 && it[1] != idHex }, - keyPair = keyPair, - createdAt = createdAt + signer = signer, + createdAt = createdAt, + onReady = onReady ) } - fun followAddressableEvent(earlierVersion: ContactListEvent, aTag: ATag, keyPair: KeyPair, createdAt: Long = TimeUtils.now()): ContactListEvent { - if (earlierVersion.isTaggedAddressableNote(aTag.toTag())) return earlierVersion + fun followAddressableEvent(earlierVersion: ContactListEvent, aTag: ATag, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (ContactListEvent) -> Unit) { + if (earlierVersion.isTaggedAddressableNote(aTag.toTag())) return return create( content = earlierVersion.content, tags = earlierVersion.tags.plus(element = listOfNotNull("a", aTag.toTag(), aTag.relay)), - keyPair = keyPair, - createdAt = createdAt + signer = signer, + createdAt = createdAt, + onReady = onReady ) } - fun unfollowAddressableEvent(earlierVersion: ContactListEvent, aTag: ATag, keyPair: KeyPair, createdAt: Long = TimeUtils.now()): ContactListEvent { - if (!earlierVersion.isTaggedAddressableNote(aTag.toTag())) return earlierVersion + fun unfollowAddressableEvent(earlierVersion: ContactListEvent, aTag: ATag, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (ContactListEvent) -> Unit) { + if (!earlierVersion.isTaggedAddressableNote(aTag.toTag())) return return create( content = earlierVersion.content, tags = earlierVersion.tags.filter { it.size > 1 && it[1] != aTag.toTag() }, - keyPair = keyPair, - createdAt = createdAt + signer = signer, + createdAt = createdAt, + onReady = onReady ) } - fun updateRelayList(earlierVersion: ContactListEvent, relayUse: Map?, keyPair: KeyPair, createdAt: Long = TimeUtils.now()): ContactListEvent { + fun updateRelayList(earlierVersion: ContactListEvent, relayUse: Map?, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (ContactListEvent) -> Unit) { val content = if (relayUse != null) { mapper.writeValueAsString(relayUse) } else { @@ -256,35 +269,20 @@ class ContactListEvent( return create( content = content, tags = earlierVersion.tags, - keyPair = keyPair, - createdAt = createdAt - ) - } - - fun create( - unsignedEvent: Event, - signature: String - ): ContactListEvent { - return ContactListEvent( - unsignedEvent.id, - unsignedEvent.pubKey, - unsignedEvent.createdAt, - unsignedEvent.tags, - unsignedEvent.content, - signature + signer = signer, + createdAt = createdAt, + onReady = onReady ) } fun create( content: String, tags: List>, - keyPair: KeyPair, - createdAt: Long = TimeUtils.now() - ): ContactListEvent { - val pubKey = keyPair.pubKey.toHexKey() - val id = generateId(pubKey, createdAt, kind, tags, content) - val sig = if (keyPair.privKey == null) null else CryptoUtils.sign(id, keyPair.privKey) - return ContactListEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig?.toHexKey() ?: "") + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (ContactListEvent) -> Unit + ) { + signer.sign(createdAt, kind, tags, content, onReady) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/DeletionEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/DeletionEvent.kt index 05b4faa93..ccb1192cf 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/DeletionEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/DeletionEvent.kt @@ -6,6 +6,7 @@ import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.crypto.KeyPair import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner @Immutable class DeletionEvent( @@ -21,17 +22,10 @@ class DeletionEvent( companion object { const val kind = 5 - fun create(deleteEvents: List, keyPair: KeyPair, createdAt: Long = TimeUtils.now()): DeletionEvent { + fun create(deleteEvents: List, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (DeletionEvent) -> Unit) { val content = "" - val pubKey = keyPair.pubKey.toHexKey() val tags = deleteEvents.map { listOf("e", it) } - val id = generateId(pubKey, createdAt, kind, tags, content) - val sig = if (keyPair.privKey == null) null else CryptoUtils.sign(id, keyPair.privKey) - return DeletionEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig?.toHexKey() ?: "") - } - - fun create(unsignedEvent: DeletionEvent, signature: String): DeletionEvent { - return DeletionEvent(unsignedEvent.id, unsignedEvent.pubKey, unsignedEvent.createdAt, unsignedEvent.tags, unsignedEvent.content, signature) + signer.sign(createdAt, kind, tags, content, onReady) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/EmojiPackEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/EmojiPackEvent.kt index 8e41d5d94..8e5e85eb2 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/EmojiPackEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/EmojiPackEvent.kt @@ -5,6 +5,7 @@ import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner @Immutable class EmojiPackEvent( @@ -21,18 +22,16 @@ class EmojiPackEvent( fun create( name: String = "", - privateKey: ByteArray, - createdAt: Long = TimeUtils.now() - ): EmojiPackEvent { + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (EmojiPackEvent) -> Unit + ) { val content = "" - val pubKey = CryptoUtils.pubkeyCreate(privateKey) val tags = mutableListOf>() tags.add(listOf("d", name)) - val id = generateId(pubKey.toHexKey(), createdAt, kind, tags, content) - val sig = CryptoUtils.sign(id, privateKey) - return EmojiPackEvent(id.toHexKey(), pubKey.toHexKey(), createdAt, tags, content, sig.toHexKey()) + signer.sign(createdAt, kind, tags, content, onReady) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/EmojiPackSelectionEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/EmojiPackSelectionEvent.kt index a73a395b1..1ee67269f 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/EmojiPackSelectionEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/EmojiPackSelectionEvent.kt @@ -7,6 +7,7 @@ import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.crypto.KeyPair import com.vitorpamplona.quartz.encoders.ATag import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner @Immutable class EmojiPackSelectionEvent( @@ -22,26 +23,18 @@ class EmojiPackSelectionEvent( fun create( listOfEmojiPacks: List?, - keyPair: KeyPair, - createdAt: Long = TimeUtils.now() - ): EmojiPackSelectionEvent { + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (EmojiPackSelectionEvent) -> Unit + ) { val msg = "" - val pubKey = keyPair.pubKey.toHexKey() val tags = mutableListOf>() listOfEmojiPacks?.forEach { tags.add(listOf("a", it.toTag())) } - val id = generateId(pubKey, createdAt, kind, tags, msg) - val sig = if (keyPair.privKey == null) null else CryptoUtils.sign(id, keyPair.privKey) - return EmojiPackSelectionEvent(id.toHexKey(), pubKey, createdAt, tags, msg, sig?.toHexKey() ?: "") - } - - fun create( - unsignedEvent: EmojiPackSelectionEvent, signature: String - ): EmojiPackSelectionEvent { - return EmojiPackSelectionEvent(unsignedEvent.id, unsignedEvent.pubKey, unsignedEvent.createdAt, unsignedEvent.tags, unsignedEvent.content, signature) + signer.sign(createdAt, kind, tags, msg, onReady) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt index 798e521ac..eb9f8c96d 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt @@ -19,6 +19,7 @@ import com.vitorpamplona.quartz.encoders.Hex import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.Nip19 import com.vitorpamplona.quartz.encoders.toHexKey +import com.vitorpamplona.quartz.signers.NostrSigner import com.vitorpamplona.quartz.utils.TimeUtils import java.math.BigDecimal import java.util.* @@ -403,11 +404,8 @@ open class Event( return CryptoUtils.sha256(makeJsonForId(pubKey, createdAt, kind, tags, content).toByteArray()) } - fun create(privateKey: ByteArray, kind: Int, tags: List> = emptyList(), content: String = "", createdAt: Long = TimeUtils.now()): Event { - val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey() - val id = Companion.generateId(pubKey, createdAt, kind, tags, content) - val sig = CryptoUtils.sign(id, privateKey).toHexKey() - return Event(id.toHexKey(), pubKey, createdAt, kind, tags, content, sig) + fun create(signer: NostrSigner, kind: Int, tags: List> = emptyList(), content: String = "", createdAt: Long = TimeUtils.now(), onReady: (Event) -> Unit) { + return signer.sign(createdAt, kind, tags, content, onReady) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt index 1069f58c9..c18ad4b67 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt @@ -4,6 +4,7 @@ import com.vitorpamplona.quartz.encoders.toHexKey class EventFactory { companion object { + fun create( id: String, pubKey: String, @@ -55,6 +56,7 @@ class EventFactory { GenericRepostEvent.kind -> GenericRepostEvent(id, pubKey, createdAt, tags, content, sig) GiftWrapEvent.kind -> GiftWrapEvent(id, pubKey, createdAt, tags, content, sig) HighlightEvent.kind -> HighlightEvent(id, pubKey, createdAt, tags, content, sig) + HTTPAuthorizationEvent.kind -> HTTPAuthorizationEvent(id, pubKey, createdAt, tags, content, sig) LiveActivitiesEvent.kind -> LiveActivitiesEvent(id, pubKey, createdAt, tags, content, sig) LiveActivitiesChatMessageEvent.kind -> LiveActivitiesChatMessageEvent(id, pubKey, createdAt, tags, content, sig) LnZapEvent.kind -> LnZapEvent(id, pubKey, createdAt, tags, content, sig) @@ -71,6 +73,7 @@ class EventFactory { PrivateDmEvent.kind -> PrivateDmEvent(id, pubKey, createdAt, tags, content, sig) ReactionEvent.kind -> ReactionEvent(id, pubKey, createdAt, tags, content, sig) RecommendRelayEvent.kind -> RecommendRelayEvent(id, pubKey, createdAt, tags, content, sig) + RelayAuthEvent.kind -> RelayAuthEvent(id, pubKey, createdAt, tags, content, sig) RelaySetEvent.kind -> RelaySetEvent(id, pubKey, createdAt, tags, content, sig) ReportEvent.kind -> ReportEvent(id, pubKey, createdAt, tags, content, sig) RepostEvent.kind -> RepostEvent(id, pubKey, createdAt, tags, content, sig) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/FileHeaderEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/FileHeaderEvent.kt index cfcf50003..af2724e73 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/FileHeaderEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/FileHeaderEvent.kt @@ -6,6 +6,7 @@ import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.crypto.KeyPair import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner @Immutable class FileHeaderEvent( @@ -56,9 +57,10 @@ class FileHeaderEvent( torrentInfoHash: String? = null, encryptionKey: AESGCM? = null, sensitiveContent: Boolean? = null, - keyPair: KeyPair, - createdAt: Long = TimeUtils.now() - ): FileHeaderEvent { + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (FileHeaderEvent) -> Unit + ) { val tags = listOfNotNull( listOf(URL, url), mimeType?.let { listOf(MIME_TYPE, mimeType) }, @@ -81,10 +83,7 @@ class FileHeaderEvent( ) val content = alt ?: "" - val pubKey = keyPair.pubKey.toHexKey() - val id = generateId(pubKey, createdAt, kind, tags, content) - val sig = if (keyPair.privKey == null) null else CryptoUtils.sign(id, keyPair.privKey) - return FileHeaderEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig?.toHexKey() ?: "") + signer.sign(createdAt, kind, tags, content, onReady) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/FileStorageEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/FileStorageEvent.kt index 010864f9e..82ad4fd94 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/FileStorageEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/FileStorageEvent.kt @@ -6,6 +6,7 @@ import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner import java.util.Base64 @Immutable @@ -43,33 +44,16 @@ class FileStorageEvent( fun create( mimeType: String, data: ByteArray, - pubKey: HexKey, - createdAt: Long = TimeUtils.now() - ): FileStorageEvent { + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (FileStorageEvent) -> Unit + ) { val tags = listOfNotNull( listOf(TYPE, mimeType) ) val content = encode(data) - val id = generateId(pubKey, createdAt, kind, tags, content) - return FileStorageEvent(id.toHexKey(), pubKey, createdAt, tags, content, "") - } - - fun create( - mimeType: String, - data: ByteArray, - privateKey: ByteArray, - createdAt: Long = TimeUtils.now() - ): FileStorageEvent { - val tags = listOfNotNull( - listOf(TYPE, mimeType) - ) - - val content = encode(data) - val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey() - val id = generateId(pubKey, createdAt, kind, tags, content) - val sig = CryptoUtils.sign(id, privateKey) - return FileStorageEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) + signer.sign(createdAt, kind, tags, content, onReady) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/FileStorageHeaderEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/FileStorageHeaderEvent.kt index 4ab80cb0a..c0d63260d 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/FileStorageHeaderEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/FileStorageHeaderEvent.kt @@ -5,6 +5,7 @@ import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner @Immutable class FileStorageHeaderEvent( @@ -53,49 +54,10 @@ class FileStorageHeaderEvent( torrentInfoHash: String? = null, encryptionKey: AESGCM? = null, sensitiveContent: Boolean? = null, - pubKey: HexKey, - createdAt: Long = TimeUtils.now() - ): FileStorageHeaderEvent { - val tags = listOfNotNull( - listOf("e", storageEvent.id), - mimeType?.let { listOf(MIME_TYPE, mimeType) }, - alt?.ifBlank { null }?.let { listOf(ALT, it) }, - hash?.let { listOf(HASH, it) }, - size?.let { listOf(FILE_SIZE, it) }, - dimensions?.let { listOf(DIMENSION, it) }, - blurhash?.let { listOf(BLUR_HASH, it) }, - magnetURI?.let { listOf(MAGNET_URI, it) }, - torrentInfoHash?.let { listOf(TORRENT_INFOHASH, it) }, - encryptionKey?.let { listOf(ENCRYPTION_KEY, it.key, it.nonce) }, - sensitiveContent?.let { - if (it) { - listOf("content-warning", "") - } else { - null - } - } - ) - - val content = alt ?: "" - val id = generateId(pubKey, createdAt, kind, tags, content) - return FileStorageHeaderEvent(id.toHexKey(), pubKey, createdAt, tags, content, "") - } - - fun create( - storageEvent: FileStorageEvent, - mimeType: String? = null, - alt: String? = null, - hash: String? = null, - size: String? = null, - dimensions: String? = null, - blurhash: String? = null, - magnetURI: String? = null, - torrentInfoHash: String? = null, - encryptionKey: AESGCM? = null, - sensitiveContent: Boolean? = null, - privateKey: ByteArray, - createdAt: Long = TimeUtils.now() - ): FileStorageHeaderEvent { + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (FileStorageHeaderEvent) -> Unit + ) { val tags = listOfNotNull( listOf("e", storageEvent.id), mimeType?.let { listOf(MIME_TYPE, mimeType) }, @@ -117,10 +79,7 @@ class FileStorageHeaderEvent( ) val content = alt ?: "" - val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey() - val id = generateId(pubKey, createdAt, kind, tags, content) - val sig = CryptoUtils.sign(id, privateKey) - return FileStorageHeaderEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) + signer.sign(createdAt, kind, tags, content, onReady) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/GeneralListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/GeneralListEvent.kt index 422e27bf6..a80c8628d 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/GeneralListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/GeneralListEvent.kt @@ -7,6 +7,7 @@ import com.vitorpamplona.quartz.encoders.hexToByteArray import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.encoders.ATag import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner @Immutable abstract class GeneralListEvent( @@ -18,6 +19,9 @@ abstract class GeneralListEvent( content: String, sig: HexKey ) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) { + @Transient + private var privateTagsCache: List>? = null + fun category() = dTag() fun bookmarkedPosts() = taggedEvents() fun bookmarkedPeople() = taggedUsers() @@ -26,80 +30,73 @@ abstract class GeneralListEvent( fun title() = tags.firstOrNull { it.size > 1 && it[0] == "title" }?.get(1) fun nameOrTitle() = name() ?: title() - fun plainContent(privKey: ByteArray): String? { - if (content.isBlank()) return null - - return try { - val sharedSecret = CryptoUtils.getSharedSecretNIP04(privKey, pubKey.hexToByteArray()) - - return CryptoUtils.decryptNIP04(content, sharedSecret) - } catch (e: Exception) { - Log.w("GeneralList", "Error decrypting the message ${e.message} for ${dTag()}") - null - } - } - - @Transient - private var privateTagsCache: List>? = null - - fun privateTags(privKey: ByteArray): List>? { - if (privateTagsCache != null) { - return privateTagsCache - } - - privateTagsCache = try { - plainContent(privKey)?.let { mapper.readValue>>(it) } - } catch (e: Throwable) { - Log.w("GeneralList", "Error parsing the JSON ${e.message}") - null - } + fun cachedPrivateTags(): List>? { return privateTagsCache } - fun privateTags(content: String): List>? { - if (privateTagsCache != null) { - return privateTagsCache + fun privateTags(signer: NostrSigner, onReady: (List>) -> Unit) { + if (content.isBlank()) return + + privateTagsCache?.let { + onReady(it) + return } - privateTagsCache = try { - content.let { mapper.readValue>>(it) } + try { + signer.nip04Decrypt(content, pubKey) { + privateTagsCache = mapper.readValue>>(it) + privateTagsCache?.let { + onReady(it) + } + } } catch (e: Throwable) { Log.w("GeneralList", "Error parsing the JSON ${e.message}") - null } - return privateTagsCache } - fun privateTagsOrEmpty(privKey: ByteArray?): List> { - if (privKey == null) return emptyList() - return privateTags(privKey) ?: emptyList() + fun privateTagsOrEmpty(signer: NostrSigner, onReady: (List>) -> Unit) { + privateTags(signer, onReady) } - fun privateTagsOrEmpty(content: String): List> { - return privateTags(content) ?: emptyList() + fun privateTaggedUsers(signer: NostrSigner, onReady: (List) -> Unit) = privateTags(signer) { + onReady(filterUsers(it)) + } + fun privateHashtags(signer: NostrSigner, onReady: (List) -> Unit) = privateTags(signer) { + onReady(filterHashtags(it)) + } + fun privateGeohashes(signer: NostrSigner, onReady: (List) -> Unit) = privateTags(signer) { + onReady(filterGeohashes(it)) + } + fun privateTaggedEvents(signer: NostrSigner, onReady: (List) -> Unit) = privateTags(signer) { + onReady(filterEvents(it)) + } + fun privateTaggedAddresses(signer: NostrSigner, onReady: (List) -> Unit) = privateTags(signer) { + onReady(filterAddresses(it)) } - fun privateTaggedUsers(privKey: ByteArray) = privateTags(privKey)?.filter { it.size > 1 && it[0] == "p" }?.map { it[1] } - fun privateTaggedUsers(content: String) = privateTags(content)?.filter { it.size > 1 && it[0] == "p" }?.map { it[1] } - fun privateHashtags(privKey: ByteArray) = privateTags(privKey)?.filter { it.size > 1 && it[0] == "t" }?.map { it[1] } - fun privateHashtags(content: String) = privateTags(content)?.filter { it.size > 1 && it[0] == "t" }?.map { it[1] } - fun privateGeohashes(privKey: ByteArray) = privateTags(privKey)?.filter { it.size > 1 && it[0] == "g" }?.map { it[1] } - fun privateGeohashes(content: String) = privateTags(content)?.filter { it.size > 1 && it[0] == "g" }?.map { it[1] } - fun privateTaggedEvents(privKey: ByteArray) = privateTags(privKey)?.filter { it.size > 1 && it[0] == "e" }?.map { it[1] } - fun privateTaggedEvents(content: String) = privateTags(content)?.filter { it.size > 1 && it[0] == "e" }?.map { it[1] } - - fun privateTaggedAddresses(privKey: ByteArray) = privateTags(privKey)?.filter { it.firstOrNull() == "a" }?.mapNotNull { - val aTagValue = it.getOrNull(1) - val relay = it.getOrNull(2) - - if (aTagValue != null) ATag.parse(aTagValue, relay) else null + fun filterUsers(tags: List>): List { + return tags.filter { it.size > 1 && it[0] == "p" }.map { it[1] } } - fun privateTaggedAddresses(content: String) = privateTags(content)?.filter { it.firstOrNull() == "a" }?.mapNotNull { - val aTagValue = it.getOrNull(1) - val relay = it.getOrNull(2) + fun filterHashtags(tags: List>): List { + return tags.filter { it.size > 1 && it[0] == "t" }.map { it[1] } + } - if (aTagValue != null) ATag.parse(aTagValue, relay) else null + fun filterGeohashes(tags: List>): List { + return tags.filter { it.size > 1 && it[0] == "g" }.map { it[1] } + } + + fun filterEvents(tags: List>): List { + return tags.filter { it.size > 1 && it[0] == "e" }.map { it[1] } + } + + fun filterAddresses(tags: List>): List { + return tags.filter { it.firstOrNull() == "a" }.mapNotNull { + val aTagValue = it.getOrNull(1) + val relay = it.getOrNull(2) + + if (aTagValue != null) ATag.parse(aTagValue, relay) else null + } } companion object { @@ -108,9 +105,9 @@ abstract class GeneralListEvent( privUsers: List? = null, privAddresses: List? = null, - privateKey: ByteArray, - pubKey: ByteArray - ): String { + signer: NostrSigner, + onReady: (String) -> Unit + ) { val privTags = mutableListOf>() privEvents?.forEach { privTags.add(listOf("e", it)) @@ -121,23 +118,21 @@ abstract class GeneralListEvent( privAddresses?.forEach { privTags.add(listOf("a", it.toTag())) } - val msg = mapper.writeValueAsString(privTags) - return CryptoUtils.encryptNIP04( - msg, - privateKey, - pubKey - ) + return encryptTags(privTags, signer, onReady) } fun encryptTags( privateTags: List>? = null, - privateKey: ByteArray - ): String { - return CryptoUtils.encryptNIP04( - msg = mapper.writeValueAsString(privateTags), - privateKey = privateKey, - pubKey = CryptoUtils.pubkeyCreate(privateKey) + signer: NostrSigner, + onReady: (String) -> Unit + ) { + val msg = mapper.writeValueAsString(privateTags) + + signer.nip04Encrypt( + msg, + signer.pubKey, + onReady ) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/GenericRepostEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/GenericRepostEvent.kt index adbea501e..765142df9 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/GenericRepostEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/GenericRepostEvent.kt @@ -6,6 +6,7 @@ import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.crypto.KeyPair import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner @Immutable class GenericRepostEvent( @@ -29,13 +30,17 @@ class GenericRepostEvent( companion object { const val kind = 16 - fun create(boostedPost: EventInterface, keyPair: KeyPair, createdAt: Long = TimeUtils.now()): GenericRepostEvent { + fun create( + boostedPost: EventInterface, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (GenericRepostEvent) -> Unit + ) { val content = boostedPost.toJson() val replyToPost = listOf("e", boostedPost.id()) val replyToAuthor = listOf("p", boostedPost.pubKey()) - val pubKey = keyPair.pubKey.toHexKey() var tags: List> = listOf(replyToPost, replyToAuthor) if (boostedPost is AddressableEvent) { @@ -44,13 +49,7 @@ class GenericRepostEvent( tags = tags + listOf(listOf("k", "${boostedPost.kind()}")) - val id = generateId(pubKey, createdAt, kind, tags, content) - val sig = if (keyPair.privKey == null) null else CryptoUtils.sign(id, keyPair.privKey) - return GenericRepostEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig?.toHexKey() ?: "") - } - - fun create(unsignedEvent: GenericRepostEvent, signature: String): GenericRepostEvent { - return GenericRepostEvent(unsignedEvent.id, unsignedEvent.pubKey, unsignedEvent.createdAt, unsignedEvent.tags, unsignedEvent.content, signature) + signer.sign(createdAt, kind, tags, content, onReady) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/GiftWrapEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/GiftWrapEvent.kt index 3946c7f9a..228cff177 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/GiftWrapEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/GiftWrapEvent.kt @@ -6,10 +6,13 @@ import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.encoders.hexToByteArray import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.crypto.CryptoUtils +import com.vitorpamplona.quartz.crypto.KeyPair import com.vitorpamplona.quartz.crypto.Nip44Version import com.vitorpamplona.quartz.crypto.decodeNIP44 import com.vitorpamplona.quartz.crypto.encodeNIP44 import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner +import com.vitorpamplona.quartz.signers.NostrSignerInternal @Immutable class GiftWrapEvent( @@ -23,66 +26,36 @@ class GiftWrapEvent( @Transient private var cachedInnerEvent: Map = mapOf() - fun cachedGift(privKey: ByteArray): Event? { - val hex = privKey.toHexKey() - if (cachedInnerEvent.contains(hex)) return cachedInnerEvent[hex] - - val myInnerEvent = unwrap(privKey = privKey) - if (myInnerEvent is WrappedEvent) { - myInnerEvent.host = this + fun cachedGift(signer: NostrSigner, onReady: (Event) -> Unit) { + cachedInnerEvent[signer.pubKey]?.let { + onReady(it) + return } - cachedInnerEvent = cachedInnerEvent + Pair(hex, myInnerEvent) - return myInnerEvent - } + unwrap(signer) { gift -> + if (gift is WrappedEvent) { + gift.host = this + } + cachedInnerEvent = cachedInnerEvent + Pair(signer.pubKey, gift) - fun cachedGift(pubKey: ByteArray, decryptedContent: String): Event? { - val hex = pubKey.toHexKey() - if (cachedInnerEvent.contains(hex)) return cachedInnerEvent[hex] - - val myInnerEvent = unwrap(decryptedContent) - if (myInnerEvent is WrappedEvent) { - myInnerEvent.host = this + onReady(gift) } - - cachedInnerEvent = cachedInnerEvent + Pair(hex, myInnerEvent) - return myInnerEvent } - fun unwrap(privKey: ByteArray) = try { - plainContent(privKey)?.let { fromJson(it) } - } catch (e: Exception) { - // Log.e("UnwrapError", "Couldn't Decrypt the content", e) - null - } - - fun unwrap(decryptedContent: String) = try { - plainContent(decryptedContent)?.let { fromJson(it) } - } catch (e: Exception) { - // Log.e("UnwrapError", "Couldn't Decrypt the content", e) - null - } - - private fun plainContent(privKey: ByteArray): String? { - if (content.isEmpty()) return null - - return try { - val toDecrypt = decodeNIP44(content) ?: return null - - return when (toDecrypt.v) { - Nip44Version.NIP04.versionCode -> CryptoUtils.decryptNIP04(toDecrypt, privKey, pubKey.hexToByteArray()) - Nip44Version.NIP44.versionCode -> CryptoUtils.decryptNIP44(toDecrypt, privKey, pubKey.hexToByteArray()) - else -> null + private fun unwrap(signer: NostrSigner, onReady: (Event) -> Unit) { + try { + plainContent(signer) { + onReady(fromJson(it)) } } catch (e: Exception) { - Log.w("GeneralList", "Error decrypting the message ${e.message}") - null + // Log.e("UnwrapError", "Couldn't Decrypt the content", e) } } - private fun plainContent(decryptedContent: String): String? { - if (decryptedContent.isEmpty()) return null - return decryptedContent + private fun plainContent(signer: NostrSigner, onReady: (String) -> Unit) { + if (content.isEmpty()) return + + signer.nip44Decrypt(content, pubKey, onReady) } fun recipientPubKey() = tags.firstOrNull { it.size > 1 && it[0] == "p" }?.get(1) @@ -93,22 +66,16 @@ class GiftWrapEvent( fun create( event: Event, recipientPubKey: HexKey, - createdAt: Long = TimeUtils.randomWithinAWeek() - ): GiftWrapEvent { - val privateKey = CryptoUtils.privkeyCreate() // GiftWrap is always a random key - val sharedSecret = CryptoUtils.getSharedSecretNIP44(privateKey, recipientPubKey.hexToByteArray()) - - val content = encodeNIP44( - CryptoUtils.encryptNIP44( - toJson(event), - sharedSecret - ) - ) - val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey() + createdAt: Long = TimeUtils.randomWithinAWeek(), + onReady: (GiftWrapEvent) -> Unit + ) { + val signer = NostrSignerInternal(KeyPair()) // GiftWrap is always a random key + val serializedContent = toJson(event) val tags = listOf(listOf("p", recipientPubKey)) - val id = generateId(pubKey, createdAt, kind, tags, content) - val sig = CryptoUtils.sign(id, privateKey) - return GiftWrapEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) + + signer.nip44Encrypt(serializedContent, recipientPubKey) { + signer.sign(createdAt, kind, tags, it, onReady) + } } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/HTTPAuthorizationEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/HTTPAuthorizationEvent.kt index c22abc29e..65d891d34 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/HTTPAuthorizationEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/HTTPAuthorizationEvent.kt @@ -6,6 +6,7 @@ import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.crypto.KeyPair import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner @Immutable class HTTPAuthorizationEvent( @@ -24,9 +25,10 @@ class HTTPAuthorizationEvent( url: String, method: String, body: String? = null, - keyPair: KeyPair, - createdAt: Long = TimeUtils.now() - ): HTTPAuthorizationEvent { + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (HTTPAuthorizationEvent) -> Unit + ) { var hash = "" body?.let { hash = CryptoUtils.sha256(it.toByteArray()).toHexKey() @@ -38,16 +40,7 @@ class HTTPAuthorizationEvent( listOf("payload", hash) ) - val pubKey = keyPair.pubKey.toHexKey() - val id = generateId(pubKey, createdAt, kind, tags, "") - val sig = if (keyPair.privKey == null) null else CryptoUtils.sign(id, keyPair.privKey) - return HTTPAuthorizationEvent(id.toHexKey(), pubKey, createdAt, tags, "", sig?.toHexKey() ?: "") - } - - fun create( - unsignedEvent: HTTPAuthorizationEvent, signature: String - ): HTTPAuthorizationEvent { - return HTTPAuthorizationEvent(unsignedEvent.id, unsignedEvent.pubKey, unsignedEvent.createdAt, unsignedEvent.tags, unsignedEvent.content, signature) + signer.sign(createdAt, kind, tags, "", onReady) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/HighlightEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/HighlightEvent.kt index 330783580..e0f5e9298 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/HighlightEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/HighlightEvent.kt @@ -5,6 +5,7 @@ import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner @Immutable class HighlightEvent( @@ -27,14 +28,11 @@ class HighlightEvent( fun create( msg: String, - privateKey: ByteArray, - createdAt: Long = TimeUtils.now() - ): HighlightEvent { - val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey() - val tags = mutableListOf>() - val id = generateId(pubKey, createdAt, kind, tags, msg) - val sig = CryptoUtils.sign(id, privateKey) - return HighlightEvent(id.toHexKey(), pubKey, createdAt, tags, msg, sig.toHexKey()) + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (HighlightEvent) -> Unit + ) { + signer.sign(createdAt, kind, emptyList(), msg, onReady) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/LiveActivitiesChatMessageEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/LiveActivitiesChatMessageEvent.kt index c0cbbb451..2a8fa8733 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/LiveActivitiesChatMessageEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/LiveActivitiesChatMessageEvent.kt @@ -7,6 +7,7 @@ import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.crypto.KeyPair import com.vitorpamplona.quartz.encoders.ATag import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner @Immutable class LiveActivitiesChatMessageEvent( @@ -50,12 +51,13 @@ class LiveActivitiesChatMessageEvent( replyTos: List? = null, mentions: List? = null, zapReceiver: List? = null, - keyPair: KeyPair, + signer: NostrSigner, createdAt: Long = TimeUtils.now(), markAsSensitive: Boolean, zapRaiserAmount: Long?, - geohash: String? = null - ): LiveActivitiesChatMessageEvent { + geohash: String? = null, + onReady: (LiveActivitiesChatMessageEvent) -> Unit + ) { val content = message val tags = mutableListOf( listOf("a", activity.toTag(), "", "root") @@ -79,16 +81,7 @@ class LiveActivitiesChatMessageEvent( tags.add(listOf("g", it)) } - val pubKey = keyPair.pubKey.toHexKey() - val id = generateId(pubKey, createdAt, kind, tags, content) - val sig = if (keyPair.privKey == null) null else CryptoUtils.sign(id, keyPair.privKey) - return LiveActivitiesChatMessageEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig?.toHexKey() ?: "") - } - - fun create( - unsignedEvent: LiveActivitiesChatMessageEvent, signature: String - ): LiveActivitiesChatMessageEvent { - return LiveActivitiesChatMessageEvent(unsignedEvent.id, unsignedEvent.pubKey, unsignedEvent.createdAt, unsignedEvent.tags, unsignedEvent.content, signature) + signer.sign(createdAt, kind, tags, content, onReady) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/LiveActivitiesEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/LiveActivitiesEvent.kt index 3cfc08d10..ad49834c9 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/LiveActivitiesEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/LiveActivitiesEvent.kt @@ -6,6 +6,7 @@ import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.encoders.ATag import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner @Immutable class LiveActivitiesEvent( @@ -50,14 +51,12 @@ class LiveActivitiesEvent( const val STATUS_ENDED = "ended" fun create( - privateKey: ByteArray, - createdAt: Long = TimeUtils.now() - ): LiveActivitiesEvent { + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (LiveActivitiesEvent) -> Unit + ) { val tags = mutableListOf>() - val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey() - val id = generateId(pubKey, createdAt, kind, tags, "") - val sig = CryptoUtils.sign(id, privateKey) - return LiveActivitiesEvent(id.toHexKey(), pubKey, createdAt, tags, "", sig.toHexKey()) + signer.sign(createdAt, kind, tags, "", onReady) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/LnZapPaymentRequestEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/LnZapPaymentRequestEvent.kt index 2f46d58f9..8a02e9a33 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/LnZapPaymentRequestEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/LnZapPaymentRequestEvent.kt @@ -11,6 +11,7 @@ import com.vitorpamplona.quartz.encoders.hexToByteArray import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner @Immutable class LnZapPaymentRequestEvent( @@ -28,24 +29,28 @@ class LnZapPaymentRequestEvent( fun walletServicePubKey() = tags.firstOrNull() { it.size > 1 && it[0] == "p" }?.get(1) - fun lnInvoice(privKey: ByteArray, pubkey: ByteArray): String? { - if (lnInvoice != null) { - return lnInvoice + fun talkingWith(oneSideHex: String): HexKey { + return if (pubKey == oneSideHex) walletServicePubKey() ?: pubKey else pubKey + } + + fun lnInvoice(signer: NostrSigner, onReady: (String) -> Unit) { + lnInvoice?.let { + onReady(it) + return } - return try { - val sharedSecret = CryptoUtils.getSharedSecretNIP04(privKey, pubkey) + try { + signer.nip04Decrypt(content, talkingWith(signer.pubKey)) { jsonText -> + val payInvoiceMethod = mapper.readValue(jsonText, Request::class.java) - val jsonText = CryptoUtils.decryptNIP04(content, sharedSecret) + lnInvoice = (payInvoiceMethod as? PayInvoiceMethod)?.params?.invoice - val payInvoiceMethod = mapper.readValue(jsonText, Request::class.java) - - lnInvoice = (payInvoiceMethod as? PayInvoiceMethod)?.params?.invoice - - return lnInvoice + lnInvoice?.let { + onReady(it) + } + } } catch (e: Exception) { Log.w("BookmarkList", "Error decrypting the message ${e.message}") - null } } @@ -55,24 +60,21 @@ class LnZapPaymentRequestEvent( fun create( lnInvoice: String, walletServicePubkey: String, - privateKey: ByteArray, - createdAt: Long = TimeUtils.now() - ): LnZapPaymentRequestEvent { - val pubKey = CryptoUtils.pubkeyCreate(privateKey) + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (LnZapPaymentRequestEvent) -> Unit + ) { val serializedRequest = mapper.writeValueAsString(PayInvoiceMethod.create(lnInvoice)) - val content = CryptoUtils.encryptNIP04( - serializedRequest, - privateKey, - walletServicePubkey.hexToByteArray() - ) - val tags = mutableListOf>() tags.add(listOf("p", walletServicePubkey)) - val id = generateId(pubKey.toHexKey(), createdAt, kind, tags, content) - val sig = CryptoUtils.sign(id, privateKey) - return LnZapPaymentRequestEvent(id.toHexKey(), pubKey.toHexKey(), createdAt, tags, content, sig.toHexKey()) + signer.nip04Encrypt( + serializedRequest, + walletServicePubkey + ) { content -> + signer.sign(createdAt, kind, tags, content, onReady) + } } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/LnZapPaymentResponseEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/LnZapPaymentResponseEvent.kt index fa6953e6e..a9c2d55fa 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/LnZapPaymentResponseEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/LnZapPaymentResponseEvent.kt @@ -9,6 +9,7 @@ import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.deser.std.StdDeserializer import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner @Immutable class LnZapPaymentResponseEvent( @@ -19,7 +20,6 @@ class LnZapPaymentResponseEvent( content: String, sig: HexKey ) : Event(id, pubKey, createdAt, kind, tags, content, sig) { - // Once one of an app user decrypts the payment, all users else can see it. @Transient private var response: Response? = null @@ -27,53 +27,37 @@ class LnZapPaymentResponseEvent( fun requestAuthor() = tags.firstOrNull() { it.size > 1 && it[0] == "p" }?.get(1) fun requestId() = tags.firstOrNull() { it.size > 1 && it[0] == "e" }?.get(1) - private fun decrypt(privKey: ByteArray, pubKey: ByteArray): String? { - return try { - val sharedSecret = CryptoUtils.getSharedSecretNIP04(privKey, pubKey) + fun talkingWith(oneSideHex: String): HexKey { + return if (pubKey == oneSideHex) requestAuthor() ?: pubKey else pubKey + } - val retVal = CryptoUtils.decryptNIP04(content, sharedSecret) - - if (retVal.startsWith(PrivateDmEvent.nip18Advertisement)) { - retVal.substring(16) - } else { - retVal + private fun plainContent(signer: NostrSigner, onReady: (String) -> Unit) { + try { + signer.nip04Decrypt(content, talkingWith(signer.pubKey)) { content -> + onReady(content) } } catch (e: Exception) { Log.w("PrivateDM", "Error decrypting the message ${e.message}") - null } } - fun response(privKey: ByteArray, pubKey: ByteArray): Response? { - if (response != null) response - - return try { - if (content.isNotEmpty()) { - val decrypted = decrypt(privKey, pubKey) - response = mapper.readValue(decrypted, Response::class.java) - response - } else { - null - } - } catch (e: Exception) { - Log.w("LnZapPaymentResponseEvent", "Can't parse content as a payment response: $content", e) - null + fun response(signer: NostrSigner, onReady: (Response) -> Unit) { + response?.let { + onReady(it) + return } - } - fun response(decryptedContent: String): Response? { - if (response != null) response - - return try { + try { if (content.isNotEmpty()) { - response = mapper.readValue(decryptedContent, Response::class.java) - response - } else { - null + plainContent(signer) { + mapper.readValue(it, Response::class.java)?.let { + response = it + onReady(it) + } + } } } catch (e: Exception) { Log.w("LnZapPaymentResponseEvent", "Can't parse content as a payment response: $content", e) - null } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/LnZapPrivateEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/LnZapPrivateEvent.kt index b3207d3ac..ddeed32e2 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/LnZapPrivateEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/LnZapPrivateEvent.kt @@ -7,6 +7,7 @@ import com.vitorpamplona.quartz.encoders.Bech32 import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.LnInvoiceUtil import com.vitorpamplona.quartz.encoders.toHexKey +import com.vitorpamplona.quartz.signers.NostrSigner import com.vitorpamplona.quartz.utils.TimeUtils import java.nio.charset.Charset import java.security.SecureRandom @@ -29,15 +30,13 @@ class LnZapPrivateEvent( const val kind = 9733 fun create( - privateKey: ByteArray, + signer: NostrSigner, tags: List> = emptyList(), content: String = "", - createdAt: Long = TimeUtils.now() - ): Event { - val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey() - val id = generateId(pubKey, createdAt, kind, tags, content) - val sig = CryptoUtils.sign(id, privateKey).toHexKey() - return Event(id.toHexKey(), pubKey, createdAt, kind, tags, content, sig) + createdAt: Long = TimeUtils.now(), + onReady: (LnZapPrivateEvent) -> Unit + ) { + signer.sign(createdAt, kind, tags, content, onReady) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/LnZapRequestEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/LnZapRequestEvent.kt index 558319eeb..1dda40cfd 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/LnZapRequestEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/LnZapRequestEvent.kt @@ -4,9 +4,12 @@ import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.utils.* import com.vitorpamplona.quartz.encoders.Bech32 import com.vitorpamplona.quartz.crypto.CryptoUtils +import com.vitorpamplona.quartz.crypto.KeyPair import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.hexToByteArray import com.vitorpamplona.quartz.encoders.toHexKey +import com.vitorpamplona.quartz.signers.NostrSigner +import com.vitorpamplona.quartz.signers.NostrSignerInternal import java.nio.charset.Charset import java.security.SecureRandom import javax.crypto.BadPaddingException @@ -34,8 +37,6 @@ class LnZapRequestEvent( fun isPrivateZap() = tags.any { t -> t.size >= 2 && t[0] == "anon" && t[1].isNotBlank() } fun getPrivateZapEvent(loggedInUserPrivKey: ByteArray, pubKey: HexKey): Event? { - if (privateZapEvent != null) return privateZapEvent - val anonTag = tags.firstOrNull { t -> t.size >= 2 && t[0] == "anon" } if (anonTag != null) { val encnote = anonTag[1] @@ -44,8 +45,7 @@ class LnZapRequestEvent( val note = decryptPrivateZapMessage(encnote, loggedInUserPrivKey, pubKey.hexToByteArray()) val decryptedEvent = fromJson(note) if (decryptedEvent.kind == 9733) { - privateZapEvent = decryptedEvent - return privateZapEvent + return decryptedEvent } } catch (e: Exception) { e.printStackTrace() @@ -55,22 +55,39 @@ class LnZapRequestEvent( return null } + fun cachedPrivateZap(): Event? { + return privateZapEvent + } + + fun decryptPrivateZap(signer: NostrSigner, onReady: (Event) -> Unit) { + privateZapEvent?.let { + onReady(it) + return + } + + signer.decryptZapEvent(this) { + // caches it + privateZapEvent = it + onReady(it) + } + } + companion object { const val kind = 9734 fun create( originalNote: EventInterface, relays: Set, - privateKey: ByteArray, + signer: NostrSigner, pollOption: Int?, message: String, zapType: LnZapEvent.ZapType, toUserPubHex: String?, // Overrides in case of Zap Splits - createdAt: Long = TimeUtils.now() - ): LnZapRequestEvent { - var content = message - var privkey = privateKey - var pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey() + createdAt: Long = TimeUtils.now(), + onReady: (LnZapRequestEvent) -> Unit + ) { + if (zapType == LnZapEvent.ZapType.NONZAP) return + var tags = listOf( listOf("e", originalNote.id()), listOf("p", toUserPubHex ?: originalNote.pubKey()), @@ -82,196 +99,43 @@ class LnZapRequestEvent( if (pollOption != null && pollOption >= 0) { tags = tags + listOf(listOf(POLL_OPTION, pollOption.toString())) } + if (zapType == LnZapEvent.ZapType.ANONYMOUS) { - tags = tags + listOf(listOf("anon", "")) - privkey = CryptoUtils.privkeyCreate() - pubKey = CryptoUtils.pubkeyCreate(privkey).toHexKey() + tags = tags + listOf(listOf("anon")) + NostrSignerInternal(KeyPair()).sign(createdAt, kind, tags, message, onReady) } else if (zapType == LnZapEvent.ZapType.PRIVATE) { - val encryptionPrivateKey = createEncryptionPrivateKey(privateKey.toHexKey(), originalNote.id(), createdAt) - val noteJson = (LnZapPrivateEvent.create(privkey, listOf(tags[0], tags[1]), message)).toJson() - val encryptedContent = encryptPrivateZapMessage(noteJson, encryptionPrivateKey, originalNote.pubKey().hexToByteArray()) - tags = tags + listOf(listOf("anon", encryptedContent)) - content = "" // make sure public content is empty, as the content is encrypted - privkey = encryptionPrivateKey // sign event with generated privkey - pubKey = CryptoUtils.pubkeyCreate(encryptionPrivateKey).toHexKey() // updated event with according pubkey + tags = tags + listOf(listOf("anon", "")) + signer.sign(createdAt, kind, tags, message, onReady) + } else { + signer.sign(createdAt, kind, tags, message, onReady) } - val id = generateId(pubKey, createdAt, kind, tags, content) - val sig = CryptoUtils.sign(id, privkey) - return LnZapRequestEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) - } - - fun create( - unsignedEvent: LnZapRequestEvent, - signature: String - ): LnZapRequestEvent { - return LnZapRequestEvent(unsignedEvent.id, unsignedEvent.pubKey, unsignedEvent.createdAt, unsignedEvent.tags, unsignedEvent.content, signature) - } - - fun createPublic( - userHex: String, - relays: Set, - pubKey: HexKey, - message: String, - createdAt: Long = TimeUtils.now() - ): LnZapRequestEvent { - val tags = listOf( - listOf("p", userHex), - listOf("relays") + relays - ) - - val id = generateId(pubKey, createdAt, kind, tags, message) - return LnZapRequestEvent(id.toHexKey(), pubKey, createdAt, tags, message, "") - } - - fun createPublic( - originalNote: EventInterface, - relays: Set, - pubKey: HexKey, - pollOption: Int?, - message: String, - toUserPubHex: String?, // Overrides in case of Zap Splits - createdAt: Long = TimeUtils.now() - ): LnZapRequestEvent { - var tags = listOf( - listOf("e", originalNote.id()), - listOf("p", toUserPubHex ?: originalNote.pubKey()), - listOf("relays") + relays - ) - if (originalNote is AddressableEvent) { - tags = tags + listOf(listOf("a", originalNote.address().toTag())) - } - if (pollOption != null && pollOption >= 0) { - tags = tags + listOf(listOf(POLL_OPTION, pollOption.toString())) - } - - val id = generateId(pubKey, createdAt, kind, tags, message) - return LnZapRequestEvent(id.toHexKey(), pubKey, createdAt, tags, message, "") - } - - fun createPrivateZap( - originalNote: EventInterface, - relays: Set, - pubKey: HexKey, - pollOption: Int?, - message: String, - toUserPubHex: String?, - createdAt: Long = TimeUtils.now() - ): LnZapRequestEvent { - val content = message - var tags = listOf( - listOf("e", originalNote.id()), - listOf("p", toUserPubHex ?: originalNote.pubKey()), - listOf("relays") + relays - ) - if (originalNote is AddressableEvent) { - tags = tags + listOf(listOf("a", originalNote.address().toTag())) - } - if (pollOption != null && pollOption >= 0) { - tags = tags + listOf(listOf(POLL_OPTION, pollOption.toString())) - } - - tags = tags + listOf(listOf("anon", "")) - - return LnZapRequestEvent("zap", pubKey, createdAt, tags, content, "") - } - - fun createPrivateZap( - userHex: String, - relays: Set, - pubKey: HexKey, - message: String, - createdAt: Long = TimeUtils.now() - ): LnZapRequestEvent { - val content = message - var tags = listOf( - listOf("p", userHex), - listOf("relays") + relays - ) - tags = tags + listOf(listOf("anon", "")) - - return LnZapRequestEvent("zap", pubKey, createdAt, tags, content, "") - } - - fun createAnonymous( - originalNote: EventInterface, - relays: Set, - pollOption: Int?, - message: String, - toUserPubHex: String?, // Overrides in case of Zap Splits - createdAt: Long = TimeUtils.now() - ): LnZapRequestEvent { - var tags = listOf( - listOf("e", originalNote.id()), - listOf("p", toUserPubHex ?: originalNote.pubKey()), - listOf("relays") + relays - ) - if (originalNote is AddressableEvent) { - tags = tags + listOf(listOf("a", originalNote.address().toTag())) - } - if (pollOption != null && pollOption >= 0) { - tags = tags + listOf(listOf(POLL_OPTION, pollOption.toString())) - } - - tags = tags + listOf(listOf("anon", "")) - val privkey = CryptoUtils.privkeyCreate() - val pubKey = CryptoUtils.pubkeyCreate(privkey).toHexKey() - - val id = generateId(pubKey, createdAt, kind, tags, message) - val sig = CryptoUtils.sign(id, privkey) - return LnZapRequestEvent(id.toHexKey(), pubKey, createdAt, tags, message, sig.toHexKey()) - } - - fun createAnonymous( - userHex: String, - relays: Set, - message: String, - createdAt: Long = TimeUtils.now() - ): LnZapRequestEvent { - var tags = listOf( - listOf("p", userHex), - listOf("relays") + relays - ) - - tags = tags + listOf(listOf("anon", "")) - val privkey = CryptoUtils.privkeyCreate() - val pubKey = CryptoUtils.pubkeyCreate(privkey).toHexKey() - - val id = generateId(pubKey, createdAt, kind, tags, message) - val sig = CryptoUtils.sign(id, privkey) - return LnZapRequestEvent(id.toHexKey(), pubKey, createdAt, tags, message, sig.toHexKey()) } fun create( userHex: String, relays: Set, - privateKey: ByteArray, + signer: NostrSigner, message: String, zapType: LnZapEvent.ZapType, - createdAt: Long = TimeUtils.now() - ): LnZapRequestEvent { - var content = message - var privkey = privateKey - var pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey() + createdAt: Long = TimeUtils.now(), + onReady: (LnZapRequestEvent) -> Unit + ) { + if (zapType == LnZapEvent.ZapType.NONZAP) return + var tags = listOf( listOf("p", userHex), listOf("relays") + relays ) + if (zapType == LnZapEvent.ZapType.ANONYMOUS) { - privkey = CryptoUtils.privkeyCreate() - pubKey = CryptoUtils.pubkeyCreate(privkey).toHexKey() tags = tags + listOf(listOf("anon", "")) + NostrSignerInternal(KeyPair()).sign(createdAt, kind, tags, message, onReady) } else if (zapType == LnZapEvent.ZapType.PRIVATE) { - val encryptionPrivateKey = createEncryptionPrivateKey(privateKey.toHexKey(), userHex, createdAt) - val noteJson = LnZapPrivateEvent.create(privkey, listOf(tags[0], tags[1]), message).toJson() - val encryptedContent = encryptPrivateZapMessage(noteJson, encryptionPrivateKey, userHex.hexToByteArray()) - tags = tags + listOf(listOf("anon", encryptedContent)) - content = "" - privkey = encryptionPrivateKey - pubKey = CryptoUtils.pubkeyCreate(encryptionPrivateKey).toHexKey() + tags = tags + listOf(listOf("anon", "")) + signer.sign(createdAt, kind, tags, message, onReady) + } else { + signer.sign(createdAt, kind, tags, message, onReady) } - val id = generateId(pubKey, createdAt, kind, tags, content) - val sig = CryptoUtils.sign(id, privkey) - return LnZapRequestEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) } @@ -281,7 +145,7 @@ class LnZapRequestEvent( return CryptoUtils.sha256(strbyte) } - private fun encryptPrivateZapMessage(msg: String, privkey: ByteArray, pubkey: ByteArray): String { + fun encryptPrivateZapMessage(msg: String, privkey: ByteArray, pubkey: ByteArray): String { val sharedSecret = CryptoUtils.getSharedSecretNIP04(privkey, pubkey) val iv = ByteArray(16) SecureRandom().nextBytes(iv) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/LongTextNoteEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/LongTextNoteEvent.kt index 09184f2a5..3d6f70489 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/LongTextNoteEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/LongTextNoteEvent.kt @@ -6,6 +6,7 @@ import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.encoders.ATag import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner @Immutable class LongTextNoteEvent( @@ -33,8 +34,14 @@ class LongTextNoteEvent( companion object { const val kind = 30023 - fun create(msg: String, replyTos: List?, mentions: List?, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): LongTextNoteEvent { - val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey() + fun create( + msg: String, + replyTos: List?, + mentions: List?, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (LongTextNoteEvent) -> Unit + ) { val tags = mutableListOf>() replyTos?.forEach { tags.add(listOf("e", it)) @@ -42,9 +49,7 @@ class LongTextNoteEvent( mentions?.forEach { tags.add(listOf("p", it)) } - val id = generateId(pubKey, createdAt, kind, tags, msg) - val sig = CryptoUtils.sign(id, privateKey) - return LongTextNoteEvent(id.toHexKey(), pubKey, createdAt, tags, msg, sig.toHexKey()) + signer.sign(createdAt, kind, tags, msg, onReady) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/MetadataEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/MetadataEvent.kt index 6bc69ddbc..70ba45abd 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/MetadataEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/MetadataEvent.kt @@ -6,6 +6,7 @@ import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner import java.io.ByteArrayInputStream @Stable @@ -149,20 +150,20 @@ class MetadataEvent( companion object { const val kind = 0 - fun create(contactMetaData: String, identities: List, pubKey: HexKey, privateKey: ByteArray?, createdAt: Long = TimeUtils.now()): MetadataEvent { + fun create( + contactMetaData: String, + identities: List, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (MetadataEvent) -> Unit + ) { val tags = mutableListOf>() identities.forEach { tags.add(listOf("i", it.platformIdentity(), it.proof)) } - val id = generateId(pubKey, createdAt, kind, tags, contactMetaData) - val sig = if (privateKey == null) null else CryptoUtils.sign(id, privateKey) - return MetadataEvent(id.toHexKey(), pubKey, createdAt, tags, contactMetaData, sig?.toHexKey() ?: "") - } - - fun create(unsignedEvent: MetadataEvent, signature: String): MetadataEvent { - return MetadataEvent(unsignedEvent.id, unsignedEvent.pubKey, unsignedEvent.createdAt, unsignedEvent.tags, unsignedEvent.content, signature) + signer.sign(createdAt, kind, tags, contactMetaData, onReady) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/MuteListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/MuteListEvent.kt index 69cca0280..607a6e2f5 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/MuteListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/MuteListEvent.kt @@ -9,6 +9,8 @@ import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.encoders.ATag import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner +import java.util.UUID @Immutable class MuteListEvent( @@ -19,41 +21,51 @@ class MuteListEvent( content: String, sig: HexKey ) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) { - fun plainContent(privKey: ByteArray): String? { - return try { - val sharedSecret = CryptoUtils.getSharedSecretNIP04(privKey, pubKey.hexToByteArray()) - - return CryptoUtils.decryptNIP04(content, sharedSecret) - } catch (e: Exception) { - Log.w("BookmarkList", "Error decrypting the message ${e.message}") - null - } - } - @Transient private var privateTagsCache: List>? = null - fun privateTags(privKey: ByteArray): List>? { - if (privateTagsCache != null) { - return privateTagsCache + private fun privateTags(signer: NostrSigner, onReady: (List>) -> Unit) { + if (content.isBlank()) return + + privateTagsCache?.let { + onReady(it) + return } - privateTagsCache = try { - plainContent(privKey)?.let { mapper.readValue>>(it) } + try { + signer.nip04Decrypt(content, pubKey) { + privateTagsCache = mapper.readValue>>(it) + privateTagsCache?.let { + onReady(it) + } + } } catch (e: Throwable) { - Log.w("BookmarkList", "Error parsing the JSON ${e.message}") - null + Log.w("MuteList", "Error parsing the JSON ${e.message}") } - return privateTagsCache } - fun privateTaggedUsers(privKey: ByteArray) = privateTags(privKey)?.filter { it.firstOrNull() == "p" }?.mapNotNull { it.getOrNull(1) } - fun privateTaggedEvents(privKey: ByteArray) = privateTags(privKey)?.filter { it.firstOrNull() == "e" }?.mapNotNull { it.getOrNull(1) } - fun privateTaggedAddresses(privKey: ByteArray) = privateTags(privKey)?.filter { it.firstOrNull() == "a" }?.mapNotNull { - val aTagValue = it.getOrNull(1) - val relay = it.getOrNull(2) + fun privateTaggedUsers(signer: NostrSigner, onReady: (List) -> Unit) = privateTags(signer) { + onReady(it.filter { it.size > 1 && it[0] == "p" }.map { it[1] } ) + } + fun privateHashtags(signer: NostrSigner, onReady: (List) -> Unit) = privateTags(signer) { + onReady(it.filter { it.size > 1 && it[0] == "t" }.map { it[1] } ) + } + fun privateGeohashes(signer: NostrSigner, onReady: (List) -> Unit) = privateTags(signer) { + onReady(it.filter { it.size > 1 && it[0] == "g" }.map { it[1] } ) + } + fun privateTaggedEvents(signer: NostrSigner, onReady: (List) -> Unit) = privateTags(signer) { + onReady(it.filter { it.size > 1 && it[0] == "e" }.map { it[1] } ) + } - if (aTagValue != null) ATag.parse(aTagValue, relay) else null + fun privateTaggedAddresses(signer: NostrSigner, onReady: (List) -> Unit) = privateTags(signer) { + onReady( + it.filter { it.firstOrNull() == "a" }.mapNotNull { + val aTagValue = it.getOrNull(1) + val relay = it.getOrNull(2) + + if (aTagValue != null) ATag.parse(aTagValue, relay) else null + } + ) } companion object { @@ -68,11 +80,10 @@ class MuteListEvent( privUsers: List? = null, privAddresses: List? = null, - privateKey: ByteArray, - createdAt: Long = TimeUtils.now() - ): MuteListEvent { - val pubKey = CryptoUtils.pubkeyCreate(privateKey) - + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (MuteListEvent) -> Unit + ) { val privTags = mutableListOf>() privEvents?.forEach { privTags.add(listOf("e", it)) @@ -85,12 +96,6 @@ class MuteListEvent( } val msg = mapper.writeValueAsString(privTags) - val content = CryptoUtils.encryptNIP04( - msg, - privateKey, - pubKey - ) - val tags = mutableListOf>() events?.forEach { tags.add(listOf("e", it)) @@ -102,9 +107,9 @@ class MuteListEvent( tags.add(listOf("a", it.toTag())) } - val id = generateId(pubKey.toHexKey(), createdAt, kind, tags, content) - val sig = CryptoUtils.sign(id, privateKey) - return MuteListEvent(id.toHexKey(), pubKey.toHexKey(), createdAt, tags, content, sig.toHexKey()) + signer.nip04Encrypt(msg, signer.pubKey) { content -> + signer.sign(createdAt, kind, tags, content, onReady) + } } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/NIP24Factory.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/NIP24Factory.kt index 44fe02a65..efdff156d 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/NIP24Factory.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/NIP24Factory.kt @@ -5,28 +5,65 @@ import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.crypto.KeyPair import com.vitorpamplona.quartz.encoders.ATag import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner +import kotlin.math.sign class NIP24Factory { data class Result(val msg: Event, val wraps: List) + private fun recursiveGiftWrapCreation( + event: Event, + remainingTos: List, + signer: NostrSigner, + output: MutableList, + onReady: (List) -> Unit + ) { + if (remainingTos.isEmpty()) { + onReady(output) + return + } + + val next = remainingTos.first() + + SealedGossipEvent.create( + event = event, + encryptTo = next, + signer = signer + ) { seal -> + GiftWrapEvent.create( + event = seal, + recipientPubKey = next + ) { giftWrap -> + output.add(giftWrap) + recursiveGiftWrapCreation(event, remainingTos.minus(next), signer, output, onReady) + } + } + } + + private fun createWraps(event: Event, to: Set, signer: NostrSigner, onReady: (List) -> Unit) { + val wraps = mutableListOf() + recursiveGiftWrapCreation(event, to.toList(), signer, wraps, onReady) + } + fun createMsgNIP24( msg: String, to: List, - keyPair: KeyPair, + signer: NostrSigner, subject: String? = null, replyTos: List? = null, mentions: List? = null, zapReceiver: List? = null, markAsSensitive: Boolean = false, zapRaiserAmount: Long? = null, - geohash: String? = null - ): Result { - val senderPublicKey = keyPair.pubKey.toHexKey() + geohash: String? = null, + onReady: (Result) -> Unit + ) { + val senderPublicKey = signer.pubKey - val senderMessage = ChatMessageEvent.create( + ChatMessageEvent.create( msg = msg, to = to, - keyPair = keyPair, + signer = signer, subject = subject, replyTos = replyTos, mentions = mentions, @@ -34,75 +71,60 @@ class NIP24Factory { markAsSensitive = markAsSensitive, zapRaiserAmount = zapRaiserAmount, geohash = geohash - ) - - return Result( - msg = senderMessage, - wraps = to.plus(senderPublicKey).map { - GiftWrapEvent.create( - event = SealedGossipEvent.create( - event = senderMessage, - encryptTo = it, - privateKey = keyPair.privKey!! - ), - recipientPubKey = it + ) { senderMessage -> + createWraps(senderMessage, to.plus(senderPublicKey).toSet(), signer) { wraps -> + onReady( + Result( + msg = senderMessage, + wraps = wraps + ) ) } - ) + } } - fun createReactionWithinGroup(content: String, originalNote: EventInterface, to: List, from: KeyPair): Result { - val senderPublicKey = from.pubKey.toHexKey() + fun createReactionWithinGroup(content: String, originalNote: EventInterface, to: List, signer: NostrSigner, onReady: (Result) -> Unit) { + val senderPublicKey = signer.pubKey - val senderReaction = ReactionEvent.create( + ReactionEvent.create( content, originalNote, - from - ) - - return Result( - msg = senderReaction, - wraps = to.plus(senderPublicKey).map { - GiftWrapEvent.create( - event = SealedGossipEvent.create( - event = senderReaction, - encryptTo = it, - privateKey = from.privKey!! - ), - recipientPubKey = it + signer + ) { senderReaction -> + createWraps(senderReaction, to.plus(senderPublicKey).toSet(), signer) { wraps-> + onReady( + Result( + msg = senderReaction, + wraps = wraps + ) ) } - ) + } } - fun createReactionWithinGroup(emojiUrl: EmojiUrl, originalNote: EventInterface, to: List, from: KeyPair): Result { - val senderPublicKey = from.pubKey.toHexKey() + fun createReactionWithinGroup(emojiUrl: EmojiUrl, originalNote: EventInterface, to: List, signer: NostrSigner, onReady: (Result) -> Unit) { + val senderPublicKey = signer.pubKey - val senderReaction = ReactionEvent.create( + ReactionEvent.create( emojiUrl, originalNote, - from - ) - - return Result( - msg = senderReaction, - wraps = to.plus(senderPublicKey).map { - GiftWrapEvent.create( - event = SealedGossipEvent.create( - event = senderReaction, - encryptTo = it, - privateKey = from.privKey!! - ), - recipientPubKey = it + signer + ) { senderReaction -> + createWraps(senderReaction, to.plus(senderPublicKey).toSet(), signer) { wraps -> + onReady( + Result( + msg = senderReaction, + wraps = wraps + ) ) } - ) + } } fun createTextNoteNIP24( msg: String, to: List, - keyPair: KeyPair, + signer: NostrSigner, replyTos: List? = null, mentions: List? = null, addresses: List?, @@ -113,13 +135,14 @@ class NIP24Factory { root: String?, directMentions: Set, zapRaiserAmount: Long? = null, - geohash: String? = null - ): Result { - val senderPublicKey = keyPair.pubKey.toHexKey() + geohash: String? = null, + onReady: (Result) -> Unit + ) { + val senderPublicKey = signer.pubKey - val senderMessage = TextNoteEvent.create( + TextNoteEvent.create( msg = msg, - keyPair = keyPair, + signer = signer, replyTos = replyTos, mentions = mentions, zapReceiver = zapReceiver, @@ -131,20 +154,15 @@ class NIP24Factory { markAsSensitive = markAsSensitive, zapRaiserAmount = zapRaiserAmount, geohash = geohash - ) - - return Result( - msg = senderMessage, - wraps = to.plus(senderPublicKey).map { - GiftWrapEvent.create( - event = SealedGossipEvent.create( - event = senderMessage, - encryptTo = it, - privateKey = keyPair.privKey!! - ), - recipientPubKey = it + ) { senderMessage -> + createWraps(senderMessage, to.plus(senderPublicKey).toSet(), signer) { wraps -> + onReady( + Result( + msg = senderMessage, + wraps = wraps + ) ) } - ) + } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/NNSEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/NNSEvent.kt index 5940bb458..a51d7b94e 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/NNSEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/NNSEvent.kt @@ -6,6 +6,7 @@ import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.encoders.ATag import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner @Immutable class NNSEvent( @@ -24,14 +25,12 @@ class NNSEvent( const val kind = 30053 fun create( - privateKey: ByteArray, - createdAt: Long = TimeUtils.now() - ): NNSEvent { + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (NNSEvent) -> Unit + ) { val tags = mutableListOf>() - val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey() - val id = generateId(pubKey, createdAt, kind, tags, "") - val sig = CryptoUtils.sign(id, privateKey) - return NNSEvent(id.toHexKey(), pubKey, createdAt, tags, "", sig.toHexKey()) + signer.sign(createdAt, kind, tags, "", onReady) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/PeopleListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/PeopleListEvent.kt index ecd928d9a..ca17519ef 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/PeopleListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/PeopleListEvent.kt @@ -5,6 +5,7 @@ import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.persistentSetOf import kotlinx.collections.immutable.toImmutableSet @@ -18,8 +19,6 @@ class PeopleListEvent( content: String, sig: HexKey ) : GeneralListEvent(id, pubKey, createdAt, kind, tags, content, sig) { - @Transient - var decryptedContent: String? = null @Transient var publicAndPrivateUserCache: ImmutableSet? = null @Transient @@ -34,69 +33,74 @@ class PeopleListEvent( return (privateUserList + publicUserList).toImmutableSet() } - fun publicAndPrivateWords(privateKey: ByteArray?): ImmutableSet { + fun publicAndPrivateWords(signer: NostrSigner, onReady: (ImmutableSet) -> Unit) { publicAndPrivateWordCache?.let { - return it + onReady(it) + return } - publicAndPrivateWordCache = filterTagList("word", privateTagsOrEmpty(privKey = privateKey)) - - return publicAndPrivateWordCache ?: persistentSetOf() + privateTagsOrEmpty(signer) { + publicAndPrivateWordCache = filterTagList("word", it) + publicAndPrivateWordCache?.let { + onReady(it) + } + } } - fun publicAndPrivateUsers(privateKey: ByteArray?): ImmutableSet { + fun publicAndPrivateUsers(signer: NostrSigner, onReady: (ImmutableSet) -> Unit) { publicAndPrivateUserCache?.let { - return it + onReady(it) + return } - publicAndPrivateUserCache = filterTagList("p", privateTagsOrEmpty(privKey = privateKey)) - - return publicAndPrivateUserCache ?: persistentSetOf() + privateTagsOrEmpty(signer) { + publicAndPrivateUserCache = filterTagList("p", it) + publicAndPrivateUserCache?.let { + onReady(it) + } + } } - fun publicAndPrivateWords(decryptedContent: String): ImmutableSet { - publicAndPrivateWordCache?.let { - return it + @Immutable + data class UsersAndWords(val users: ImmutableSet, val words: ImmutableSet) + + fun publicAndPrivateUsersAndWords(signer: NostrSigner, onReady: (UsersAndWords) -> Unit) { + publicAndPrivateUserCache?.let { userList -> + publicAndPrivateWordCache?.let { wordList -> + onReady(UsersAndWords(userList, wordList)) + return + } } - publicAndPrivateWordCache = filterTagList("word", privateTagsOrEmpty(decryptedContent)) + privateTagsOrEmpty(signer) { + publicAndPrivateUserCache = filterTagList("p", it) + publicAndPrivateWordCache = filterTagList("word", it) - return publicAndPrivateWordCache ?: persistentSetOf() - } - - fun publicAndPrivateUsers(decryptedContent: String): ImmutableSet { - publicAndPrivateUserCache?.let { - return it + publicAndPrivateUserCache?.let { userList -> + publicAndPrivateWordCache?.let { wordList -> + onReady( + UsersAndWords(userList, wordList) + ) + } + } } - - publicAndPrivateUserCache = filterTagList("p", privateTagsOrEmpty(decryptedContent)) - - return publicAndPrivateUserCache ?: persistentSetOf() } - fun isTagged(key: String, tag: String, isPrivate: Boolean, privateKey: ByteArray): Boolean { + fun isTagged(key: String, tag: String, isPrivate: Boolean, signer: NostrSigner, onReady: (Boolean) -> Unit) { return if (isPrivate) { - privateTagsOrEmpty(privKey = privateKey).any { it.size > 1 && it[0] == key && it[1] == tag } + privateTagsOrEmpty(signer = signer) { + onReady( + it.any { it.size > 1 && it[0] == key && it[1] == tag } + ) + } } else { - isTagged(key, tag) + onReady(isTagged(key, tag)) } } - fun isTagged(key: String, tag: String, isPrivate: Boolean, decryptedContent: String): Boolean { - return if (isPrivate) { - privateTagsOrEmpty(decryptedContent).any { it.size > 1 && it[0] == key && it[1] == tag } - } else { - isTagged(key, tag) - } - } + fun isTaggedWord(word: String, isPrivate: Boolean, signer: NostrSigner, onReady: (Boolean) -> Unit) = isTagged( "word", word, isPrivate, signer, onReady) - fun isTaggedWord(word: String, isPrivate: Boolean, privateKey: ByteArray) = isTagged( "word", word, isPrivate, privateKey) - - fun isTaggedUser(idHex: String, isPrivate: Boolean, privateKey: ByteArray) = isTagged( "p", idHex, isPrivate, privateKey) - - fun isTaggedUser(idHex: String, isPrivate: Boolean, decryptedContent: String) = isTagged( "p", idHex, isPrivate, decryptedContent) - - fun isTaggedWord(idHex: String, isPrivate: Boolean, decryptedContent: String) = isTagged( "word", idHex, isPrivate, decryptedContent) + fun isTaggedUser(idHex: String, isPrivate: Boolean, signer: NostrSigner, onReady: (Boolean) -> Unit) = isTagged( "p", idHex, isPrivate, signer, onReady) companion object { const val kind = 30000 @@ -106,83 +110,56 @@ class PeopleListEvent( return "30000:$pubKeyHex:$blockList" } - fun createListWithTag(name: String, key: String, tag: String, isPrivate: Boolean, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): PeopleListEvent { - return if (isPrivate) { - create( - content = encryptTags(listOf(listOf(key, tag)), privateKey), - tags = listOf(listOf("d", name)), - privateKey = privateKey, - createdAt = createdAt - ) + fun createListWithTag(name: String, key: String, tag: String, isPrivate: Boolean, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (PeopleListEvent) -> Unit) { + if (isPrivate) { + encryptTags(listOf(listOf(key, tag)), signer) { encryptedTags -> + create( + content = encryptedTags, + tags = listOf(listOf("d", name)), + signer = signer, + createdAt = createdAt, + onReady = onReady + ) + } } else { create( content = "", tags = listOf(listOf("d", name), listOf(key, tag)), - privateKey = privateKey, - createdAt = createdAt + signer = signer, + createdAt = createdAt, + onReady = onReady ) } } - fun createListWithUser(name: String, pubKeyHex: String, isPrivate: Boolean, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): PeopleListEvent { - return createListWithTag(name, "p", pubKeyHex, isPrivate, privateKey, createdAt) + fun createListWithUser(name: String, pubKeyHex: String, isPrivate: Boolean, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (PeopleListEvent) -> Unit) { + return createListWithTag(name, "p", pubKeyHex, isPrivate, signer, createdAt, onReady) } - fun createListWithWord(name: String, word: String, isPrivate: Boolean, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): PeopleListEvent { - return createListWithTag(name, "word", word, isPrivate, privateKey, createdAt) + fun createListWithWord(name: String, word: String, isPrivate: Boolean, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (PeopleListEvent) -> Unit) { + return createListWithTag(name, "word", word, isPrivate, signer, createdAt, onReady) } - fun createListWithUser(name: String, pubKeyHex: String, isPrivate: Boolean, pubKey: HexKey, encryptedContent: String, createdAt: Long = TimeUtils.now()): PeopleListEvent { - return if (isPrivate) { - create( - content = encryptedContent, - tags = listOf(listOf("d", name)), - pubKey = pubKey, - createdAt = createdAt - ) - } else { - create( - content = "", - tags = listOf(listOf("d", name), listOf("p", pubKeyHex)), - pubKey = pubKey, - createdAt = createdAt - ) - } - } - - fun createListWithWord(name: String, word: String, isPrivate: Boolean, pubKey: HexKey, encryptedContent: String, createdAt: Long = TimeUtils.now()): PeopleListEvent { - return if (isPrivate) { - create( - content = encryptedContent, - tags = listOf(listOf("d", name)), - pubKey = pubKey, - createdAt = createdAt - ) - } else { - create( - content = "", - tags = listOf(listOf("d", name), listOf("word", word)), - pubKey = pubKey, - createdAt = createdAt - ) - } - } - - fun addUsers(earlierVersion: PeopleListEvent, listPubKeyHex: List, isPrivate: Boolean, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): PeopleListEvent { - return if (isPrivate) { - create( - content = encryptTags( - privateTags = earlierVersion.privateTagsOrEmpty(privKey = privateKey).plus( + fun addUsers(earlierVersion: PeopleListEvent, listPubKeyHex: List, isPrivate: Boolean, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (PeopleListEvent) -> Unit) { + if (isPrivate) { + earlierVersion.privateTagsOrEmpty(signer) { privateTags -> + encryptTags( + privateTags = privateTags.plus( listPubKeyHex.map { listOf("p", it) } ), - privateKey = privateKey - ), - tags = earlierVersion.tags, - privateKey = privateKey, - createdAt = createdAt - ) + signer = signer + ) { encryptedTags -> + create( + content = encryptedTags, + tags = earlierVersion.tags, + signer = signer, + createdAt = createdAt, + onReady = onReady + ) + } + } } else { create( content = earlierVersion.content, @@ -191,143 +168,99 @@ class PeopleListEvent( listOf("p", it) } ), - privateKey = privateKey, - createdAt = createdAt + signer = signer, + createdAt = createdAt, + onReady = onReady ) } } - fun addWord(earlierVersion: PeopleListEvent, word: String, isPrivate: Boolean, pubKey: HexKey, encryptedContent: String, createdAt: Long = TimeUtils.now()): PeopleListEvent { - return if (isPrivate) { - create( - content = encryptedContent, - tags = earlierVersion.tags, - pubKey = pubKey, - createdAt = createdAt - ) - } else { - create( - content = earlierVersion.content, - tags = earlierVersion.tags.plus(element = listOf("word", word)), - pubKey = pubKey, - createdAt = createdAt - ) + fun addWord(earlierVersion: PeopleListEvent, word: String, isPrivate: Boolean, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (PeopleListEvent) -> Unit) { + return addTag(earlierVersion, "word", word, isPrivate, signer, createdAt, onReady) + } + + fun addUser(earlierVersion: PeopleListEvent, pubKeyHex: String, isPrivate: Boolean, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (PeopleListEvent) -> Unit) { + return addTag(earlierVersion, "p", pubKeyHex, isPrivate, signer, createdAt, onReady) + } + + fun addTag(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.plus(element = listOf(key, tag)), + signer = signer + ) { encryptedTags -> + create( + content = encryptedTags, + tags = earlierVersion.tags, + signer = signer, + createdAt = createdAt, + onReady = onReady + ) + } + } + } else { + create( + content = earlierVersion.content, + tags = earlierVersion.tags.plus(element = listOf(key, tag)), + signer = signer, + createdAt = createdAt, + onReady = onReady + ) + } + } } } - fun addUser(earlierVersion: PeopleListEvent, pubKeyHex: String, isPrivate: Boolean, pubKey: HexKey, encryptedContent: String, createdAt: Long = TimeUtils.now()): PeopleListEvent { - return if (isPrivate) { - create( - content = encryptedContent, - tags = earlierVersion.tags, - pubKey = pubKey, - createdAt = createdAt - ) - } else { - create( - content = earlierVersion.content, - tags = earlierVersion.tags.plus(element = listOf("p", pubKeyHex)), - pubKey = pubKey, - createdAt = createdAt - ) + fun removeWord(earlierVersion: PeopleListEvent, word: String, isPrivate: Boolean, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (PeopleListEvent) -> Unit) { + return removeTag(earlierVersion, "word", word, isPrivate, signer, createdAt, onReady) + } + + fun removeUser(earlierVersion: PeopleListEvent, pubKeyHex: String, isPrivate: Boolean, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (PeopleListEvent) -> Unit) { + return removeTag(earlierVersion, "p", pubKeyHex, isPrivate, 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) }, + signer = signer + ) { encryptedTags -> + create( + content = encryptedTags, + tags = earlierVersion.tags.filter { it.size > 1 && !(it[0] == key && it[1] == tag) }, + signer = signer, + createdAt = createdAt, + onReady = onReady + ) + } + } + } else { + create( + content = earlierVersion.content, + tags = earlierVersion.tags.filter { it.size > 1 && !(it[0] == key && it[1] == tag) }, + signer = signer, + createdAt = createdAt, + onReady = onReady + ) + } + } } } - fun removeTag(earlierVersion: PeopleListEvent, tag: String, isPrivate: Boolean, pubKey: HexKey, encryptedContent: String, createdAt: Long = TimeUtils.now()): PeopleListEvent { - return if (isPrivate) { - create( - content = encryptedContent, - tags = earlierVersion.tags, - pubKey = pubKey, - createdAt = createdAt - ) - } else { - create( - content = earlierVersion.content, - tags = earlierVersion.tags.filter { it.size > 1 && it[1] != tag }, - pubKey = pubKey, - createdAt = createdAt - ) - } - } - - fun addWord(earlierVersion: PeopleListEvent, word: String, isPrivate: Boolean, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): PeopleListEvent { - return addTag(earlierVersion, "word", word, isPrivate, privateKey, createdAt) - } - - fun addUser(earlierVersion: PeopleListEvent, pubKeyHex: String, isPrivate: Boolean, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): PeopleListEvent { - return addTag(earlierVersion, "p", pubKeyHex, isPrivate, privateKey, createdAt) - } - - - fun addTag(earlierVersion: PeopleListEvent, key: String, tag: String, isPrivate: Boolean, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): PeopleListEvent { - if (earlierVersion.isTagged(key, tag, isPrivate, privateKey)) return earlierVersion - - return if (isPrivate) { - create( - content = encryptTags( - privateTags = earlierVersion.privateTagsOrEmpty(privKey = privateKey).plus(element = listOf(key, tag)), - privateKey = privateKey - ), - tags = earlierVersion.tags, - privateKey = privateKey, - createdAt = createdAt - ) - } else { - create( - content = earlierVersion.content, - tags = earlierVersion.tags.plus(element = listOf(key, tag)), - privateKey = privateKey, - createdAt = createdAt - ) - } - } - - fun removeWord(earlierVersion: PeopleListEvent, word: String, isPrivate: Boolean, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): PeopleListEvent { - return removeTag(earlierVersion, "word", word, isPrivate, privateKey, createdAt) - } - - fun removeUser(earlierVersion: PeopleListEvent, pubKeyHex: String, isPrivate: Boolean, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): PeopleListEvent { - return removeTag(earlierVersion, "p", pubKeyHex, isPrivate, privateKey, createdAt) - } - - fun removeTag(earlierVersion: PeopleListEvent, key: String, tag: String, isPrivate: Boolean, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): PeopleListEvent { - if (!earlierVersion.isTagged(key, tag, isPrivate, privateKey)) return earlierVersion - - return if (isPrivate) { - create( - content = encryptTags( - privateTags = earlierVersion.privateTagsOrEmpty(privKey = privateKey).filter { it.size > 1 && !(it[0] == key && it[1] == tag) }, - privateKey = privateKey - ), - tags = earlierVersion.tags.filter { it.size > 1 && !(it[0] == key && it[1] == tag) }, - privateKey = privateKey, - createdAt = createdAt - ) - } else { - create( - content = earlierVersion.content, - tags = earlierVersion.tags.filter { it.size > 1 && !(it[0] == key && it[1] == tag) }, - privateKey = privateKey, - createdAt = createdAt - ) - } - } - - fun create(content: String, tags: List>, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): PeopleListEvent { - val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey() - val id = generateId(pubKey, createdAt, kind, tags, content) - val sig = CryptoUtils.sign(id, privateKey) - return PeopleListEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) - } - - fun create(content: String, tags: List>, pubKey: HexKey, createdAt: Long = TimeUtils.now()): PeopleListEvent { - val id = generateId(pubKey, createdAt, kind, tags, content) - return PeopleListEvent(id.toHexKey(), pubKey, createdAt, tags, content, "") - } - - fun create(unsignedEvent: PeopleListEvent, signature: String): PeopleListEvent { - return PeopleListEvent(unsignedEvent.id, unsignedEvent.pubKey, unsignedEvent.createdAt, unsignedEvent.tags, unsignedEvent.content, signature) + fun create( + content: String, + tags: List>, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (PeopleListEvent) -> Unit + ) { + signer.sign(createdAt, kind, tags, content, onReady) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/PinListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/PinListEvent.kt index b1a719655..49daf8634 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/PinListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/PinListEvent.kt @@ -6,6 +6,7 @@ import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.encoders.ATag import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner @Immutable class PinListEvent( @@ -24,18 +25,16 @@ class PinListEvent( fun create( pins: List, - privateKey: ByteArray, - createdAt: Long = TimeUtils.now() - ): PinListEvent { + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (PinListEvent) -> Unit + ) { val tags = mutableListOf>() pins.forEach { tags.add(listOf("pin", it)) } - val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey() - val id = generateId(pubKey, createdAt, kind, tags, "") - val sig = CryptoUtils.sign(id, privateKey) - return PinListEvent(id.toHexKey(), pubKey, createdAt, tags, "", sig.toHexKey()) + signer.sign(createdAt, kind, tags, "", onReady) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/PollNoteEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/PollNoteEvent.kt index 739964019..963498349 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/PollNoteEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/PollNoteEvent.kt @@ -6,6 +6,7 @@ import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.encoders.ATag import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner const val POLL_OPTION = "poll_option" const val VALUE_MAXIMUM = "value_maximum" @@ -48,8 +49,7 @@ class PollNoteEvent( replyTos: List?, mentions: List?, addresses: List?, - pubKey: HexKey, - privateKey: ByteArray?, + signer: NostrSigner, createdAt: Long = TimeUtils.now(), pollOptions: Map, valueMaximum: Int?, @@ -59,8 +59,9 @@ class PollNoteEvent( zapReceiver: List? = null, markAsSensitive: Boolean, zapRaiserAmount: Long?, - geohash: String? = null - ): PollNoteEvent { + geohash: String? = null, + onReady: (PollNoteEvent) -> Unit + ) { val tags = mutableListOf>() replyTos?.forEach { tags.add(listOf("e", it)) @@ -99,15 +100,7 @@ class PollNoteEvent( tags.add(listOf("g", it)) } - val id = generateId(pubKey, createdAt, kind, tags, msg) - val sig = if (privateKey == null) null else CryptoUtils.sign(id, privateKey) - return PollNoteEvent(id.toHexKey(), pubKey, createdAt, tags, msg, sig?.toHexKey() ?: "") - } - - fun create( - unsignedEvent: PollNoteEvent, signature: String - ): PollNoteEvent { - return PollNoteEvent(unsignedEvent.id, unsignedEvent.pubKey, unsignedEvent.createdAt, unsignedEvent.tags, unsignedEvent.content, signature) + signer.sign(createdAt, kind, tags, msg, onReady) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/PrivateDmEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/PrivateDmEvent.kt index 02af9953a..46802bf0c 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/PrivateDmEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/PrivateDmEvent.kt @@ -9,7 +9,10 @@ import com.vitorpamplona.quartz.crypto.KeyPair import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.HexValidator import com.vitorpamplona.quartz.encoders.Hex +import com.vitorpamplona.quartz.encoders.hexToByteArray +import com.vitorpamplona.quartz.signers.NostrSigner import kotlinx.collections.immutable.persistentSetOf +import java.util.UUID @Immutable class PrivateDmEvent( @@ -20,6 +23,9 @@ class PrivateDmEvent( content: String, sig: HexKey ) : Event(id, pubKey, createdAt, kind, tags, content, sig), ChatroomKeyable { + @Transient + private var decryptedContent: Map = mapOf() + /** * This may or may not be the actual recipient's pub key. The event is intended to look like a * nip-04 EncryptedDmEvent but may omit the recipient, too. This value can be queried and used @@ -59,20 +65,26 @@ class PrivateDmEvent( tags.any { it.size > 1 && it[0] == "p" && it[1] == pubkeyHex } } - fun plainContent(privKey: ByteArray, pubKey: ByteArray): String? { - return try { - val sharedSecret = CryptoUtils.getSharedSecretNIP04(privKey, pubKey) + fun cachedContentFor(signer: NostrSigner): String? { + return decryptedContent[signer.pubKey] + } - val retVal = CryptoUtils.decryptNIP04(content, sharedSecret) + fun plainContent(signer: NostrSigner, onReady: (String) -> Unit) { + decryptedContent[signer.pubKey]?.let { + onReady(it) + return + } - if (retVal.startsWith(nip18Advertisement)) { + signer.nip04Decrypt(content, talkingWith(signer.pubKey)) { retVal -> + val content = if (retVal.startsWith(nip18Advertisement)) { retVal.substring(16) } else { retVal } - } catch (e: Exception) { - Log.w("PrivateDM", "Error decrypting the message ${e.message}") - null + + decryptedContent = decryptedContent + Pair(signer.pubKey, content) + + onReady(content) } } @@ -82,28 +94,24 @@ class PrivateDmEvent( const val nip18Advertisement = "[//]: # (nip18)\n" fun create( - recipientPubKey: ByteArray, + recipientPubKey: HexKey, msg: String, replyTos: List? = null, mentions: List? = null, zapReceiver: List? = null, - keyPair: KeyPair, + signer: NostrSigner, createdAt: Long = TimeUtils.now(), - publishedRecipientPubKey: ByteArray? = null, + publishedRecipientPubKey: HexKey? = null, advertiseNip18: Boolean = true, markAsSensitive: Boolean, zapRaiserAmount: Long?, - geohash: String? = null - ): PrivateDmEvent { + geohash: String? = null, + onReady: (PrivateDmEvent) -> Unit + ) { val message = if (advertiseNip18) { nip18Advertisement } else { "" } + msg - val content = if (keyPair.privKey == null) message else CryptoUtils.encryptNIP04( - message, - keyPair.privKey, - recipientPubKey - ) val tags = mutableListOf>() publishedRecipientPubKey?.let { - tags.add(listOf("p", publishedRecipientPubKey.toHexKey())) + tags.add(listOf("p", publishedRecipientPubKey)) } replyTos?.forEach { tags.add(listOf("e", it)) @@ -124,60 +132,9 @@ class PrivateDmEvent( tags.add(listOf("g", it)) } - val pubKey = keyPair.pubKey.toHexKey() - val id = generateId(pubKey, createdAt, kind, tags, content) - val sig = if (keyPair.privKey == null) null else CryptoUtils.sign(id, keyPair.privKey) - return PrivateDmEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig?.toHexKey() ?: "") - } - - fun createWithoutSignature( - msg: String, - replyTos: List? = null, - mentions: List? = null, - zapReceiver: List? = null, - keyPair: KeyPair, - createdAt: Long = TimeUtils.now(), - publishedRecipientPubKey: ByteArray? = null, - advertiseNip18: Boolean = true, - markAsSensitive: Boolean, - zapRaiserAmount: Long?, - geohash: String? = null - ): PrivateDmEvent { - val message = if (advertiseNip18) { nip18Advertisement } else { "" } + msg - val content = message - val tags = mutableListOf>() - publishedRecipientPubKey?.let { - tags.add(listOf("p", publishedRecipientPubKey.toHexKey())) + signer.nip04Encrypt(message, recipientPubKey) { content -> + signer.sign(createdAt, kind, tags, content, onReady) } - replyTos?.forEach { - tags.add(listOf("e", it)) - } - mentions?.forEach { - tags.add(listOf("p", it)) - } - zapReceiver?.forEach { - tags.add(listOf("zap", it.lnAddressOrPubKeyHex, it.relay ?: "", it.weight.toString())) - } - if (markAsSensitive) { - tags.add(listOf("content-warning", "")) - } - zapRaiserAmount?.let { - tags.add(listOf("zapraiser", "$it")) - } - geohash?.let { - tags.add(listOf("g", it)) - } - - val pubKey = keyPair.pubKey.toHexKey() - val id = generateId(pubKey, createdAt, kind, tags, content) - return PrivateDmEvent(id.toHexKey(), pubKey, createdAt, tags, content, "") - } - - fun create( - unsignedEvent: PrivateDmEvent, - signature: String, - ): PrivateDmEvent { - return PrivateDmEvent(unsignedEvent.id, unsignedEvent.pubKey, unsignedEvent.createdAt, unsignedEvent.tags, unsignedEvent.content, signature) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/ReactionEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/ReactionEvent.kt index fcf8a1a9e..4aae2bcc7 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/ReactionEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/ReactionEvent.kt @@ -6,6 +6,7 @@ import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.crypto.KeyPair import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner @Immutable class ReactionEvent( @@ -23,29 +24,25 @@ class ReactionEvent( companion object { const val kind = 7 - fun createWarning(originalNote: EventInterface, keyPair: KeyPair, createdAt: Long = TimeUtils.now()): ReactionEvent { - return create("\u26A0\uFE0F", originalNote, keyPair, createdAt) + fun createWarning(originalNote: EventInterface, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (ReactionEvent) -> Unit) { + return create("\u26A0\uFE0F", originalNote, signer, createdAt, onReady) } - fun createLike(originalNote: EventInterface, keyPair: KeyPair, createdAt: Long = TimeUtils.now()): ReactionEvent { - return create("+", originalNote, keyPair, createdAt) + fun createLike(originalNote: EventInterface, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (ReactionEvent) -> Unit) { + return create("+", originalNote, signer, createdAt, onReady) } - fun create(content: String, originalNote: EventInterface, keyPair: KeyPair, createdAt: Long = TimeUtils.now()): ReactionEvent { + fun create(content: String, originalNote: EventInterface, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (ReactionEvent) -> Unit) { var tags = listOf(listOf("e", originalNote.id()), listOf("p", originalNote.pubKey())) if (originalNote is AddressableEvent) { tags = tags + listOf(listOf("a", originalNote.address().toTag())) } - val pubKey = keyPair.pubKey.toHexKey() - val id = generateId(pubKey, createdAt, kind, tags, content) - val sig = if (keyPair.privKey == null) null else CryptoUtils.sign(id, keyPair.privKey) - return ReactionEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig?.toHexKey() ?: "") + return signer.sign(createdAt, kind, tags, content, onReady) } - fun create(emojiUrl: EmojiUrl, originalNote: EventInterface, keyPair: KeyPair, createdAt: Long = TimeUtils.now()): ReactionEvent { + fun create(emojiUrl: EmojiUrl, originalNote: EventInterface, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (ReactionEvent) -> Unit) { val content = ":${emojiUrl.code}:" - val pubKey = keyPair.pubKey.toHexKey() var tags = listOf( listOf("e", originalNote.id()), @@ -57,13 +54,7 @@ class ReactionEvent( tags = tags + listOf(listOf("a", originalNote.address().toTag())) } - val id = generateId(pubKey, createdAt, kind, tags, content) - val sig = if (keyPair.privKey == null) null else CryptoUtils.sign(id, keyPair.privKey) - return ReactionEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig?.toHexKey() ?: "") - } - - fun create(unsignedEvent: ReactionEvent, signature: String): ReactionEvent { - return ReactionEvent(unsignedEvent.id, unsignedEvent.pubKey, unsignedEvent.createdAt, unsignedEvent.tags, unsignedEvent.content, signature) + signer.sign(createdAt, kind, tags, content, onReady) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/RecommendRelayEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/RecommendRelayEvent.kt index ec6dd23ed..1f8d52f29 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/RecommendRelayEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/RecommendRelayEvent.kt @@ -5,6 +5,7 @@ import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner import java.net.URI @Immutable @@ -22,13 +23,15 @@ class RecommendRelayEvent( companion object { const val kind = 2 - fun create(relay: URI, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): RecommendRelayEvent { + fun create( + relay: URI, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (RecommendRelayEvent) -> Unit + ) { val content = relay.toString() - val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey() val tags = listOf>() - val id = generateId(pubKey, createdAt, kind, tags, content) - val sig = CryptoUtils.sign(id, privateKey) - return RecommendRelayEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) + signer.sign(createdAt, kind, tags, content, onReady) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/RelayAuthEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/RelayAuthEvent.kt index 6cb829cae..4df5ea009 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/RelayAuthEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/RelayAuthEvent.kt @@ -5,6 +5,7 @@ import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner @Immutable class RelayAuthEvent( @@ -21,20 +22,19 @@ class RelayAuthEvent( companion object { const val kind = 22242 - fun create(relay: String, challenge: String, pubKey: HexKey, privateKey: ByteArray?, createdAt: Long = TimeUtils.now()): RelayAuthEvent { + fun create( + relay: String, + challenge: String, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (RelayAuthEvent) -> Unit + ) { val content = "" val tags = listOf( listOf("relay", relay), listOf("challenge", challenge) ) - val localPubKey = if (pubKey.isBlank() && privateKey != null) CryptoUtils.pubkeyCreate(privateKey).toHexKey() else pubKey - val id = generateId(localPubKey, createdAt, kind, tags, content) - val sig = if (privateKey == null) null else CryptoUtils.sign(id, privateKey) - return RelayAuthEvent(id.toHexKey(), localPubKey, createdAt, tags, content, sig?.toHexKey() ?: "") - } - - fun create(unsignedEvent: RelayAuthEvent, signature: String): RelayAuthEvent { - return RelayAuthEvent(unsignedEvent.id, unsignedEvent.pubKey, unsignedEvent.createdAt, unsignedEvent.tags, unsignedEvent.content, signature) + signer.sign(createdAt, kind, tags, content, onReady) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/RelaySetEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/RelaySetEvent.kt index cebff1e06..3d9d6a159 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/RelaySetEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/RelaySetEvent.kt @@ -6,6 +6,7 @@ import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.encoders.ATag import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner @Immutable class RelaySetEvent( @@ -24,18 +25,16 @@ class RelaySetEvent( fun create( relays: List, - privateKey: ByteArray, - createdAt: Long = TimeUtils.now() - ): RelaySetEvent { + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (RelaySetEvent) -> Unit + ) { val tags = mutableListOf>() relays.forEach { tags.add(listOf("r", it)) } - val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey() - val id = generateId(pubKey, createdAt, kind, tags, "") - val sig = CryptoUtils.sign(id, privateKey) - return RelaySetEvent(id.toHexKey(), pubKey, createdAt, tags, "", sig.toHexKey()) + signer.sign(createdAt, kind, tags, "", onReady) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/ReportEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/ReportEvent.kt index d4436db62..cb38759ec 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/ReportEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/ReportEvent.kt @@ -6,6 +6,7 @@ import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.crypto.KeyPair import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner @Immutable data class ReportedKey(val key: String, val reportType: ReportEvent.ReportType) @@ -57,35 +58,36 @@ class ReportEvent( fun create( reportedPost: EventInterface, type: ReportType, - keyPair: KeyPair, + signer: NostrSigner, content: String = "", - createdAt: Long = TimeUtils.now() - ): ReportEvent { + createdAt: Long = TimeUtils.now(), + onReady: (ReportEvent) -> Unit + ) { val reportPostTag = listOf("e", reportedPost.id(), type.name.lowercase()) val reportAuthorTag = listOf("p", reportedPost.pubKey(), type.name.lowercase()) - val pubKey = keyPair.pubKey.toHexKey() var tags: List> = listOf(reportPostTag, reportAuthorTag) if (reportedPost is AddressableEvent) { tags = tags + listOf(listOf("a", reportedPost.address().toTag())) } - val id = generateId(pubKey, createdAt, kind, tags, content) - val sig = if (keyPair.privKey == null) null else CryptoUtils.sign(id, keyPair.privKey) - return ReportEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig?.toHexKey() ?: "") + signer.sign(createdAt, kind, tags, content, onReady) } - fun create(reportedUser: String, type: ReportType, keyPair: KeyPair, createdAt: Long = TimeUtils.now()): ReportEvent { + fun create( + reportedUser: String, + type: ReportType, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (ReportEvent) -> Unit + ) { val content = "" val reportAuthorTag = listOf("p", reportedUser, type.name.lowercase()) - val pubKey = keyPair.pubKey.toHexKey() val tags: List> = listOf(reportAuthorTag) - val id = generateId(pubKey, createdAt, kind, tags, content) - val sig = if (keyPair.privKey == null) null else CryptoUtils.sign(id, keyPair.privKey) - return ReportEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig?.toHexKey() ?: "") + signer.sign(createdAt, kind, tags, content, onReady) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/RepostEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/RepostEvent.kt index 0a09b98e3..3b522b494 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/RepostEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/RepostEvent.kt @@ -6,6 +6,7 @@ import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.crypto.KeyPair import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner @Immutable class RepostEvent( @@ -29,26 +30,24 @@ class RepostEvent( companion object { const val kind = 6 - fun create(boostedPost: EventInterface, keyPair: KeyPair, createdAt: Long = TimeUtils.now()): RepostEvent { + fun create( + boostedPost: EventInterface, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (RepostEvent) -> Unit + ) { val content = boostedPost.toJson() val replyToPost = listOf("e", boostedPost.id()) val replyToAuthor = listOf("p", boostedPost.pubKey()) - val pubKey = keyPair.pubKey.toHexKey() var tags: List> = listOf(replyToPost, replyToAuthor) if (boostedPost is AddressableEvent) { tags = tags + listOf(listOf("a", boostedPost.address().toTag())) } - val id = generateId(pubKey, createdAt, kind, tags, content) - val sig = if (keyPair.privKey == null) null else CryptoUtils.sign(id, keyPair.privKey) - return RepostEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig?.toHexKey() ?: "") - } - - fun create(unsignedEvent: RepostEvent, signature: String): RepostEvent { - return RepostEvent(unsignedEvent.id, unsignedEvent.pubKey, unsignedEvent.createdAt, unsignedEvent.tags, unsignedEvent.content, signature) + signer.sign(createdAt, kind, tags, content, onReady) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/SealedGossipEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/SealedGossipEvent.kt index 2e3e38fee..4f78569a2 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/SealedGossipEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/SealedGossipEvent.kt @@ -11,6 +11,8 @@ import com.vitorpamplona.quartz.crypto.Nip44Version import com.vitorpamplona.quartz.crypto.decodeNIP44 import com.vitorpamplona.quartz.crypto.encodeNIP44 import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner +import java.util.UUID @Immutable class SealedGossipEvent( @@ -24,119 +26,66 @@ class SealedGossipEvent( @Transient private var cachedInnerEvent: Map = mapOf() - fun cachedGossip(privKey: ByteArray): Event? { - val hex = privKey.toHexKey() - if (cachedInnerEvent.contains(hex)) return cachedInnerEvent[hex] - - val gossip = unseal(privKey = privKey) - val event = gossip?.mergeWith(this) - if (event is WrappedEvent) { - event.host = host ?: this + fun cachedGossip(signer: NostrSigner, onReady: (Event) -> Unit) { + cachedInnerEvent[signer.pubKey]?.let { + onReady(it) + return } - cachedInnerEvent = cachedInnerEvent + Pair(hex, event) - return event - } + unseal(signer) { gossip -> + val event = gossip.mergeWith(this) + if (event is WrappedEvent) { + event.host = host ?: this + } - fun cachedGossip(pubKey: ByteArray, decryptedContent: String): Event? { - val hex = pubKey.toHexKey() - if (cachedInnerEvent.contains(hex)) return cachedInnerEvent[hex] - - val gossip = unseal(decryptedContent) - val event = gossip?.mergeWith(this) - if (event is WrappedEvent) { - event.host = host ?: this + cachedInnerEvent = cachedInnerEvent + Pair(signer.pubKey, event) + onReady(event) } - - cachedInnerEvent = cachedInnerEvent + Pair(hex, event) - return event } - fun unseal(privKey: ByteArray): Gossip? = try { - plainContent(privKey)?.let { Gossip.fromJson(it) } - } catch (e: Exception) { - Log.w("GossipEvent", "Fail to decrypt or parse Gossip", e) - null - } - - fun unseal(decryptedContent: String): Gossip? = try { - plainContent(decryptedContent)?.let { Gossip.fromJson(it) } - } catch (e: Exception) { - Log.w("GossipEvent", "Fail to decrypt or parse Gossip", e) - null - } - - private fun plainContent(decryptedContent: String): String? { - if (decryptedContent.isEmpty()) return null - return decryptedContent - } - - private fun plainContent(privKey: ByteArray): String? { - if (content.isEmpty()) return null - - return try { - val toDecrypt = decodeNIP44(content) ?: return null - - return when (toDecrypt.v) { - Nip44Version.NIP04.versionCode -> CryptoUtils.decryptNIP04(toDecrypt, privKey, pubKey.hexToByteArray()) - Nip44Version.NIP44.versionCode -> CryptoUtils.decryptNIP44(toDecrypt, privKey, pubKey.hexToByteArray()) - else -> null + private fun unseal(signer: NostrSigner, onReady: (Gossip) -> Unit) { + try { + plainContent(signer) { + onReady(Gossip.fromJson(it)) } } catch (e: Exception) { - Log.w("GossipEvent", "Error decrypting the message ${e.message}") - null + Log.w("GossipEvent", "Fail to decrypt or parse Gossip", e) } } + private fun plainContent(signer: NostrSigner, onReady: (String) -> Unit) { + if (content.isEmpty()) return + + signer.nip44Decrypt(content, pubKey, onReady) + } + companion object { const val kind = 13 fun create( event: Event, encryptTo: HexKey, - privateKey: ByteArray, - createdAt: Long = TimeUtils.now() - ): SealedGossipEvent { + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (SealedGossipEvent) -> Unit + ) { val gossip = Gossip.create(event) - return create(gossip, encryptTo, privateKey, createdAt) + create(gossip, encryptTo, signer, createdAt, onReady) } fun create( gossip: Gossip, encryptTo: HexKey, - privateKey: ByteArray, - createdAt: Long = TimeUtils.randomWithinAWeek() - ): SealedGossipEvent { - val sharedSecret = CryptoUtils.getSharedSecretNIP44(privateKey, encryptTo.hexToByteArray()) - - val content = encodeNIP44( - CryptoUtils.encryptNIP44( - Gossip.toJson(gossip), - sharedSecret - ) - ) - val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey() + signer: NostrSigner, + createdAt: Long = TimeUtils.randomWithinAWeek(), + onReady: (SealedGossipEvent) -> Unit + ) { + val msg = Gossip.toJson(gossip) val tags = listOf>() - val id = generateId(pubKey, createdAt, kind, tags, content) - val sig = CryptoUtils.sign(id, privateKey) - return SealedGossipEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) - } - fun create( - encryptedContent: String, - pubKey: HexKey, - createdAt: Long = TimeUtils.randomWithinAWeek() - ): SealedGossipEvent { - val tags = listOf>() - val id = generateId(pubKey, createdAt, kind, tags, encryptedContent) - return SealedGossipEvent(id.toHexKey(), pubKey, createdAt, tags, encryptedContent, "") - } - - fun create( - unsignedEvent: SealedGossipEvent, - signature: String - ): SealedGossipEvent { - return SealedGossipEvent(unsignedEvent.id, unsignedEvent.pubKey, unsignedEvent.createdAt, unsignedEvent.tags, unsignedEvent.content, signature) + signer.nip44Encrypt(msg, encryptTo) { content -> + signer.sign(createdAt, kind, tags, content, onReady) + } } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/StatusEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/StatusEvent.kt index d9b03f7a6..4cbe1953e 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/StatusEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/StatusEvent.kt @@ -7,6 +7,7 @@ import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.crypto.KeyPair import com.vitorpamplona.quartz.encoders.ATag import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner @Immutable class StatusEvent( @@ -25,51 +26,38 @@ class StatusEvent( msg: String, type: String, expiration: Long?, - keyPair: KeyPair, - createdAt: Long = TimeUtils.now() - ): StatusEvent { + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (StatusEvent) -> Unit + ) { val tags = mutableListOf>() tags.add(listOf("d", type)) expiration?.let { tags.add(listOf("expiration", it.toString())) } - val pubKey = keyPair.pubKey.toHexKey() - val id = generateId(pubKey, createdAt, kind, tags, msg) - val sig = if (keyPair.privKey == null) null else CryptoUtils.sign(id, keyPair.privKey) - return StatusEvent(id.toHexKey(), pubKey, createdAt, tags, msg, sig?.toHexKey() ?: "") + signer.sign(createdAt, kind, tags, msg, onReady) } fun update( event: StatusEvent, newStatus: String, - keyPair: KeyPair, - createdAt: Long = TimeUtils.now() - ): StatusEvent { + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (StatusEvent) -> Unit + ) { val tags = event.tags - val pubKey = event.pubKey() - val id = generateId(pubKey, createdAt, kind, tags, newStatus) - val sig = if (keyPair.privKey == null) null else CryptoUtils.sign(id, keyPair.privKey) - return StatusEvent(id.toHexKey(), pubKey, createdAt, tags, newStatus, sig?.toHexKey() ?: "") + signer.sign(createdAt, kind, tags, newStatus, onReady) } fun clear( event: StatusEvent, - keyPair: KeyPair, - createdAt: Long = TimeUtils.now() - ): StatusEvent { + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (StatusEvent) -> Unit + ) { val msg = "" val tags = event.tags.filter { it.size > 1 && it[0] == "d" } - val pubKey = event.pubKey() - val id = generateId(pubKey, createdAt, kind, tags, msg) - val sig = if (keyPair.privKey == null) null else CryptoUtils.sign(id, keyPair.privKey) - return StatusEvent(id.toHexKey(), pubKey, createdAt, tags, msg, sig?.toHexKey() ?: "") - } - - fun create( - unsignedEvent: StatusEvent, - signature: String - ): StatusEvent { - return StatusEvent(unsignedEvent.id, unsignedEvent.pubKey, unsignedEvent.createdAt, unsignedEvent.tags, unsignedEvent.content, signature) + signer.sign(createdAt, kind, tags, msg, onReady) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/TextNoteEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/TextNoteEvent.kt index 737e262b5..c8fb02d38 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/TextNoteEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/TextNoteEvent.kt @@ -9,6 +9,7 @@ import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.crypto.KeyPair import com.vitorpamplona.quartz.encoders.ATag import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner @Immutable class TextNoteEvent( @@ -38,9 +39,10 @@ class TextNoteEvent( root: String?, directMentions: Set, geohash: String? = null, - keyPair: KeyPair, - createdAt: Long = TimeUtils.now() - ): TextNoteEvent { + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (TextNoteEvent) -> Unit + ) { val tags = mutableListOf>() replyTos?.forEach { if (it == root) { @@ -95,17 +97,7 @@ class TextNoteEvent( tags.add(listOf("g", it)) } - val pubKey = keyPair.pubKey.toHexKey() - val id = generateId(pubKey, createdAt, kind, tags, msg) - val sig = if (keyPair.privKey == null) null else CryptoUtils.sign(id, keyPair.privKey) - return TextNoteEvent(id.toHexKey(), pubKey, createdAt, tags, msg, sig?.toHexKey() ?: "") - } - - fun create( - unsignedEvent: TextNoteEvent, signature: String - ): TextNoteEvent { - - return TextNoteEvent(unsignedEvent.id, unsignedEvent.pubKey, unsignedEvent.createdAt, unsignedEvent.tags, unsignedEvent.content, signature) + signer.sign(createdAt, kind, tags, msg, onReady) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/signers/ExternalSignerLauncher.kt b/quartz/src/main/java/com/vitorpamplona/quartz/signers/ExternalSignerLauncher.kt new file mode 100644 index 000000000..c50d6bce4 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/signers/ExternalSignerLauncher.kt @@ -0,0 +1,200 @@ +package com.vitorpamplona.quartz.signers + +import android.content.ContentResolver +import android.content.Intent +import android.net.Uri +import android.util.Log +import android.util.LruCache +import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.events.EventInterface +import com.vitorpamplona.quartz.events.LnZapRequestEvent + +enum class SignerType { + SIGN_EVENT, + NIP04_ENCRYPT, + NIP04_DECRYPT, + NIP44_ENCRYPT, + NIP44_DECRYPT, + GET_PUBLIC_KEY, + DECRYPT_ZAP_EVENT +} + +class ExternalSignerLauncher( + private val npub: String, + private val signerPackageName: String = "com.greenart7c3.nostrsigner" +) { + private val contentCache = LruCache Unit>(20) + + private var signerAppLauncher: ((Intent) -> Unit)? = null + private var contentResolver: (() -> ContentResolver)? = null + + /** + * Call this function when the launcher becomes available on activity, fragment or compose + */ + fun registerLauncher( + launcher: ((Intent) -> Unit), + contentResolver: (() -> ContentResolver), + ) { + this.signerAppLauncher = launcher + this.contentResolver = contentResolver + } + + /** + * Call this function when the activity is destroyed or is about to be replaced. + */ + fun clearLauncher() { + this.signerAppLauncher = null + this.contentResolver = null + } + + fun newResult(data: Intent) { + val signature = data.getStringExtra("signature") ?: "" + val id = data.getStringExtra("id") ?: "" + if (id.isNotBlank()) { + contentCache.get(id)?.invoke(signature) + } + } + + + fun openSignerApp( + data: String, + type: SignerType, + pubKey: HexKey, + id: String, + onReady: (String)-> Unit + ) { + signerAppLauncher?.let { + openSignerApp( + data, type, it, pubKey, id, onReady + ) + } + } + + private fun openSignerApp( + data: String, + type: SignerType, + intentLauncher: (Intent) -> Unit, + pubKey: HexKey, + id: String, + onReady: (String)-> Unit + ) { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse("nostrsigner:$data")) + val signerType = when (type) { + SignerType.SIGN_EVENT -> "sign_event" + SignerType.NIP04_ENCRYPT -> "nip04_encrypt" + SignerType.NIP04_DECRYPT -> "nip04_decrypt" + SignerType.NIP44_ENCRYPT -> "nip44_encrypt" + SignerType.NIP44_DECRYPT -> "nip44_decrypt" + SignerType.GET_PUBLIC_KEY -> "get_public_key" + SignerType.DECRYPT_ZAP_EVENT -> "decrypt_zap_event" + } + intent.putExtra("type", signerType) + intent.putExtra("pubKey", pubKey) + intent.putExtra("id", id) + if (type !== SignerType.GET_PUBLIC_KEY) { + intent.putExtra("current_user", npub) + } + intent.`package` = signerPackageName + + contentCache.put(id, onReady) + + intentLauncher(intent) + } + + fun openSigner(event: EventInterface, columnName: String = "signature", onReady: (String)-> Unit) { + val result = getDataFromResolver(SignerType.SIGN_EVENT, arrayOf(event.toJson(), event.pubKey()), columnName) + if (result == null) { + openSignerApp( + event.toJson(), + SignerType.SIGN_EVENT, + "", + event.id(), + onReady + ) + } else { + onReady(result) + } + } + + fun getDataFromResolver(signerType: SignerType, data: Array, columnName: String = "signature"): String? { + return contentResolver?.let { it() }?.let { + getDataFromResolver(signerType, data, columnName, it) + } + } + + fun getDataFromResolver(signerType: SignerType, data: Array, columnName: String = "signature", contentResolver: ContentResolver): String? { + val localData = if (signerType !== SignerType.GET_PUBLIC_KEY) { + data.toList().plus(npub).toTypedArray() + } else { + data + } + + contentResolver.query( + Uri.parse("content://${signerPackageName}.$signerType"), + localData, + null, + null, + null + ).use { + if (it == null) { + return null + } + if (it.moveToFirst()) { + val index = it.getColumnIndex(columnName) + if (index < 0) { + Log.d("getDataFromResolver", "column '$columnName' not found") + return null + } + return it.getString(index) + } + } + return null + } + + fun decrypt(encryptedContent: String, pubKey: HexKey, signerType: SignerType = SignerType.NIP04_DECRYPT, onReady: (String)-> Unit) { + val id = (encryptedContent + pubKey).hashCode().toString() + val result = getDataFromResolver(signerType, arrayOf(encryptedContent, pubKey)) + if (result == null) { + openSignerApp( + encryptedContent, + signerType, + pubKey, + id, + onReady + ) + } else { + onReady(result) + } + } + + fun encrypt(decryptedContent: String, pubKey: HexKey, signerType: SignerType = SignerType.NIP04_ENCRYPT, onReady: (String)-> Unit) { + val id = (decryptedContent + pubKey).hashCode().toString() + val result = getDataFromResolver(signerType, arrayOf(decryptedContent, pubKey)) + if (result == null) { + openSignerApp( + decryptedContent, + signerType, + pubKey, + id, + onReady + ) + } else { + onReady(result) + } + } + + fun decryptZapEvent(event: LnZapRequestEvent, onReady: (String)-> Unit) { + val result = getDataFromResolver(SignerType.DECRYPT_ZAP_EVENT, arrayOf(event.toJson(), event.pubKey)) + if (result == null) { + openSignerApp( + event.toJson(), + SignerType.DECRYPT_ZAP_EVENT, + event.pubKey, + event.id, + onReady + ) + } else { + onReady(result) + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/signers/NostrSigner.kt b/quartz/src/main/java/com/vitorpamplona/quartz/signers/NostrSigner.kt new file mode 100644 index 000000000..e9ce32145 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/signers/NostrSigner.kt @@ -0,0 +1,23 @@ +package com.vitorpamplona.quartz.signers + +import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.events.Event +import com.vitorpamplona.quartz.events.EventFactory +import com.vitorpamplona.quartz.events.LnZapRequestEvent +import com.vitorpamplona.quartz.events.PeopleListEvent +import kotlinx.collections.immutable.ImmutableSet +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +abstract class NostrSigner(val pubKey: HexKey) { + + abstract fun sign(createdAt: Long, kind: Int, tags: List>, content: String, onReady: (T) -> Unit) + + abstract fun nip04Encrypt(decryptedContent: String, toPublicKey: HexKey, onReady: (String)-> Unit) + abstract fun nip04Decrypt(encryptedContent: String, fromPublicKey: HexKey, onReady: (String)-> Unit) + + abstract fun nip44Encrypt(decryptedContent: String, toPublicKey: HexKey, onReady: (String)-> Unit) + abstract fun nip44Decrypt(encryptedContent: String, fromPublicKey: HexKey, onReady: (String)-> Unit) + + abstract fun decryptZapEvent(event: LnZapRequestEvent, onReady: (Event)-> Unit) +} \ No newline at end of file diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/signers/NostrSignerExternal.kt b/quartz/src/main/java/com/vitorpamplona/quartz/signers/NostrSignerExternal.kt new file mode 100644 index 000000000..b159e0742 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/signers/NostrSignerExternal.kt @@ -0,0 +1,110 @@ +package com.vitorpamplona.quartz.signers + +import android.util.Log +import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.encoders.hexToByteArray +import com.vitorpamplona.quartz.encoders.toHexKey +import com.vitorpamplona.quartz.encoders.toNpub +import com.vitorpamplona.quartz.events.Event +import com.vitorpamplona.quartz.events.EventFactory +import com.vitorpamplona.quartz.events.LnZapRequestEvent + +class NostrSignerExternal( + pubKey: HexKey, + val launcher: ExternalSignerLauncher = ExternalSignerLauncher(pubKey.hexToByteArray().toNpub()), +): NostrSigner(pubKey) { + + override fun sign( + createdAt: Long, + kind: Int, + tags: List>, + content: String, + onReady: (T) -> Unit + ) { + val id = Event.generateId(pubKey, createdAt, kind, tags, content).toHexKey() + + val event = Event( + id = id, + pubKey = pubKey, + createdAt = createdAt, + kind = kind, + tags = tags, + content = content, + sig = "" + ) + + launcher.openSigner(event) { signature -> + (EventFactory.create( + event.id, + event.pubKey, + event.createdAt, + event.kind, + event.tags, + event.content, + signature + ) as? T?)?.let { + onReady(it) + } + } + } + + override fun nip04Encrypt(decryptedContent: String, toPublicKey: HexKey, onReady: (String)-> Unit) { + Log.d("NostrExternalSigner", "Encrypt NIP04 Event: ${decryptedContent}") + + return launcher.encrypt( + decryptedContent, + toPublicKey, + SignerType.NIP04_ENCRYPT, + onReady + ) + } + + override fun nip04Decrypt(encryptedContent: String, fromPublicKey: HexKey, onReady: (String)-> Unit) { + Log.d("NostrExternalSigner", "Decrypt NIP04 Event: ${encryptedContent}") + + return launcher.decrypt( + encryptedContent, + fromPublicKey, + SignerType.NIP04_DECRYPT, + onReady + ) + } + + override fun nip44Encrypt(decryptedContent: String, toPublicKey: HexKey, onReady: (String)-> Unit) { + Log.d("NostrExternalSigner", "Encrypt NIP44 Event: ${decryptedContent}") + + return launcher.encrypt( + decryptedContent, + pubKey, + SignerType.NIP44_ENCRYPT, + onReady + ) + } + + override fun nip44Decrypt(encryptedContent: String, fromPublicKey: HexKey, onReady: (String)-> Unit) { + Log.d("NostrExternalSigner", "Decrypt NIP44 Event: ${encryptedContent}") + + return launcher.decrypt( + encryptedContent, + pubKey, + SignerType.NIP44_DECRYPT, + onReady + ) + } + + override fun decryptZapEvent(event: LnZapRequestEvent, onReady: (Event)-> Unit) { + return launcher.decryptZapEvent(event) { + val event = try { + Event.fromJson(it) + } catch( e: Exception) { + Log.e("NostrExternalSigner", "Unable to parse returned decrypted Zap: ${it}") + null + } + event?.let { + onReady(event) + } + } + } + + +} \ No newline at end of file diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/signers/NostrSignerInternal.kt b/quartz/src/main/java/com/vitorpamplona/quartz/signers/NostrSignerInternal.kt new file mode 100644 index 000000000..ea6a5805d --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/signers/NostrSignerInternal.kt @@ -0,0 +1,218 @@ +package com.vitorpamplona.quartz.signers + +import android.util.Log +import com.vitorpamplona.quartz.crypto.CryptoUtils +import com.vitorpamplona.quartz.crypto.KeyPair +import com.vitorpamplona.quartz.crypto.Nip44Version +import com.vitorpamplona.quartz.crypto.decodeNIP44 +import com.vitorpamplona.quartz.crypto.encodeNIP44 +import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.encoders.hexToByteArray +import com.vitorpamplona.quartz.encoders.toHexKey +import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent +import com.vitorpamplona.quartz.events.Event +import com.vitorpamplona.quartz.events.EventFactory +import com.vitorpamplona.quartz.events.LnZapPrivateEvent +import com.vitorpamplona.quartz.events.LnZapRequestEvent +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class NostrSignerInternal(val keyPair: KeyPair): NostrSigner(keyPair.pubKey.toHexKey()) { + override fun sign( + createdAt: Long, + kind: Int, + tags: List>, + content: String, + onReady: (T)-> Unit + ) { + if (keyPair.privKey == null) return + + if (isUnsignedPrivateEvent(kind, tags)) { + // this is a private zap + signPrivateZap(createdAt, kind, tags, content, onReady) + } else { + signNormal(createdAt, kind, tags, content, onReady) + } + } + + fun isUnsignedPrivateEvent( + kind: Int, + tags: List>, + ): Boolean { + return kind == LnZapRequestEvent.kind && tags.any { t -> t.size > 1 && t[0] == "anon" && t[1].isBlank() } + } + + fun signNormal( + createdAt: Long, + kind: Int, + tags: List>, + content: String, + onReady: (T)-> Unit + ) { + if (keyPair.privKey == null) return + + val id = Event.generateId(pubKey, createdAt, kind, tags, content) + val sig = CryptoUtils.sign(id, keyPair.privKey).toHexKey() + + onReady( + EventFactory.create( + id.toHexKey(), + pubKey, + createdAt, + kind, + tags, + content, + sig + ) as T // Must never crash + ) + } + + override fun nip04Encrypt(decryptedContent: String, toPublicKey: HexKey, onReady: (String)-> Unit) { + if (keyPair.privKey == null) return + + onReady( + CryptoUtils.encryptNIP04( + decryptedContent, + keyPair.privKey, + toPublicKey.hexToByteArray() + ) + ) + } + + override fun nip04Decrypt(encryptedContent: String, fromPublicKey: HexKey, onReady: (String)-> Unit) { + if (keyPair.privKey == null) return + + try { + val sharedSecret = CryptoUtils.getSharedSecretNIP04(keyPair.privKey, fromPublicKey.hexToByteArray()) + + onReady(CryptoUtils.decryptNIP04(encryptedContent, sharedSecret)) + } catch (e: Exception) { + Log.w("NIP04Decrypt", "Error decrypting the message ${e.message} on ${encryptedContent}") + } + } + + override fun nip44Encrypt(decryptedContent: String, toPublicKey: HexKey, onReady: (String)-> Unit) { + if (keyPair.privKey == null) return + + val sharedSecret = CryptoUtils.getSharedSecretNIP44(keyPair.privKey, toPublicKey.hexToByteArray()) + + onReady( + encodeNIP44( + CryptoUtils.encryptNIP44( + decryptedContent, + sharedSecret + ) + ) + ) + } + + override fun nip44Decrypt(encryptedContent: String, fromPublicKey: HexKey, onReady: (String)-> Unit) { + if (keyPair.privKey == null) return + + val toDecrypt = decodeNIP44(encryptedContent) ?: return + + when (toDecrypt.v) { + Nip44Version.NIP04.versionCode -> CryptoUtils.decryptNIP04(toDecrypt, keyPair.privKey, fromPublicKey.hexToByteArray()) + Nip44Version.NIP44.versionCode -> CryptoUtils.decryptNIP44(toDecrypt, keyPair.privKey, fromPublicKey.hexToByteArray()) + else -> null + }?.let { + onReady(it) + } + } + + private fun signPrivateZap( + createdAt: Long, + kind: Int, + tags: List>, + content: String, + onReady: (T)-> Unit + ) { + if (keyPair.privKey == null) return + + val zappedEvent = tags.firstOrNull { it.size > 1 && it[0] == "e" }?.let { it[1] } + val userHex = tags.firstOrNull { it.size > 1 && it[0] == "p" }?.let { it[1] } ?: return + + // if it is a Zap for an Event, use event.id if not, use the user's pubkey + val idToGeneratePrivateKey = zappedEvent ?: userHex + + val encryptionPrivateKey = + LnZapRequestEvent.createEncryptionPrivateKey(keyPair.privKey.toHexKey(), idToGeneratePrivateKey, createdAt) + + val fullTagsNoAnon = tags.filter { t -> t.getOrNull(0) != "anon" } + + LnZapPrivateEvent.create(this, fullTagsNoAnon, content) { + val noteJson = it.toJson() + val encryptedContent = LnZapRequestEvent.encryptPrivateZapMessage( + noteJson, + encryptionPrivateKey, + userHex.hexToByteArray() + ) + + val newTags = tags.filter { t -> t.getOrNull(0) != "anon" } + listOf(listOf("anon", encryptedContent)) + val newContent = "" + + NostrSignerInternal(KeyPair(encryptionPrivateKey)).signNormal(createdAt, kind, newTags, newContent, onReady) + } + } + + override fun decryptZapEvent(event: LnZapRequestEvent, onReady: (Event)-> Unit) { + if (keyPair.privKey == null) return + + val recipientPK = event.zappedAuthor().firstOrNull() + val recipientPost = event.zappedPost().firstOrNull() + val privateEvent = if (recipientPK == pubKey) { + // if the receiver is logged in, these are the params. + val privateKeyToUse = keyPair.privKey + val pubkeyToUse = event.pubKey + + event.getPrivateZapEvent(privateKeyToUse, pubkeyToUse) + } else { + // if the sender is logged in, these are the params + val altPubkeyToUse = recipientPK + val altPrivateKeyToUse = if (recipientPost != null) { + LnZapRequestEvent.createEncryptionPrivateKey( + keyPair.privKey.toHexKey(), + recipientPost, + event.createdAt + ) + } else if (recipientPK != null) { + LnZapRequestEvent.createEncryptionPrivateKey( + keyPair.privKey.toHexKey(), + recipientPK, + event.createdAt + ) + } else { + null + } + + try { + if (altPrivateKeyToUse != null && altPubkeyToUse != null) { + val altPubKeyFromPrivate = CryptoUtils.pubkeyCreate(altPrivateKeyToUse).toHexKey() + + if (altPubKeyFromPrivate == event.pubKey) { + val result = event.getPrivateZapEvent(altPrivateKeyToUse, altPubkeyToUse) + + if (result == null) { + Log.w( + "Private ZAP Decrypt", + "Fail to decrypt Zap from ${event.id}" + ) + } + result + } else { + null + } + } else { + null + } + } catch (e: Exception) { + Log.e("Account", "Failed to create pubkey for ZapRequest ${event.id}", e) + null + } + } + + privateEvent?.let { + onReady(it) + } + } +} \ No newline at end of file