From f9272f5cfc88eeb37b21e161d684157fac8bf656 Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Mon, 21 Aug 2023 14:48:11 -0300 Subject: [PATCH] implemented amber support for auth event and the notifications --- .../notifications/PushNotificationUtils.kt | 3 +- app/src/main/AndroidManifest.xml | 1 + .../vitorpamplona/amethyst/ServiceManager.kt | 7 ++ .../vitorpamplona/amethyst/model/Account.kt | 6 +- .../amethyst/service/IntentUtils.kt | 9 +++ .../service/NostrAccountDataSource.kt | 38 +++++++-- .../NostrLnZapPaymentResponseDataSource.kt | 2 +- .../amethyst/service/PackageUtils.kt | 2 +- .../service/notifications/RegisterAccounts.kt | 45 +++++++++-- .../vitorpamplona/amethyst/ui/MainActivity.kt | 80 +++++++++++++++++-- .../amethyst/ui/actions/SignerDialog.kt | 8 +- .../notifications/PushNotificationUtils.kt | 6 +- .../quartz/events/RelayAuthEvent.kt | 7 +- 13 files changed, 181 insertions(+), 33 deletions(-) create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/service/IntentUtils.kt diff --git a/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt b/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt index af98585d05..0fcc508163 100644 --- a/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt +++ b/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt @@ -2,7 +2,8 @@ package com.vitorpamplona.amethyst.service.notifications import com.vitorpamplona.amethyst.AccountInfo -class PushNotificationUtils { +object PushNotificationUtils { + var hasInit: Boolean = true fun init(accounts: List) { } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c03aa2f1dd..841e803ab1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -4,6 +4,7 @@ + diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt b/app/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt index 0d4de5610d..d8231eb15d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt @@ -34,6 +34,8 @@ import com.vitorpamplona.amethyst.ui.actions.ImageUploader import java.io.File object 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 fun start(account: Account, context: Context) { @@ -43,6 +45,9 @@ object ServiceManager { @Synchronized fun start(context: Context) { + if (isStarted && account != null) { + return + } Log.d("ServiceManager", "Starting Relay Services") val myAccount = account @@ -87,6 +92,7 @@ object ServiceManager { NostrSingleEventDataSource.start() NostrSingleChannelDataSource.start() NostrSingleUserDataSource.start() + isStarted = true } } @@ -112,6 +118,7 @@ object ServiceManager { NostrVideoDataSource.stop() Client.disconnect() + isStarted = false } fun cleanUp() { 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 a4774fc252..dff781c9ad 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -1354,10 +1354,10 @@ class Account( LocalCache.consume(event) } - fun createAuthEvent(relay: Relay, challenge: String): RelayAuthEvent? { - if (!isWriteable()) return null + fun createAuthEvent(relay: Relay, challenge: String, isAmberInstalled: Boolean): RelayAuthEvent? { + if (!isWriteable() && !isAmberInstalled) return null - return RelayAuthEvent.create(relay.url, challenge, keyPair.privKey!!) + return RelayAuthEvent.create(relay.url, challenge, keyPair.pubKey.toHexKey(), keyPair.privKey) } fun removePublicBookmark(note: Note) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/IntentUtils.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/IntentUtils.kt new file mode 100644 index 0000000000..383eb3edaf --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/IntentUtils.kt @@ -0,0 +1,9 @@ +package com.vitorpamplona.amethyst.service + +import android.content.Intent +import androidx.activity.result.ActivityResultLauncher + +object IntentUtils { + lateinit var activityResultLauncher: ActivityResultLauncher + lateinit var authActivityResultLauncher: ActivityResultLauncher +} 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 2f4c8311ba..10efa074ed 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.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.model.LocalCache import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES @@ -8,17 +9,27 @@ import com.vitorpamplona.amethyst.service.relays.EOSEAccount import com.vitorpamplona.amethyst.service.relays.JsonFilter import com.vitorpamplona.amethyst.service.relays.Relay import com.vitorpamplona.amethyst.service.relays.TypedFilter -import com.vitorpamplona.quartz.events.* +import com.vitorpamplona.amethyst.ui.actions.SignerType +import com.vitorpamplona.amethyst.ui.actions.openAmber +import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent import com.vitorpamplona.quartz.events.BadgeAwardEvent import com.vitorpamplona.quartz.events.BadgeProfilesEvent import com.vitorpamplona.quartz.events.BookmarkListEvent import com.vitorpamplona.quartz.events.ChannelMessageEvent import com.vitorpamplona.quartz.events.ContactListEvent +import com.vitorpamplona.quartz.events.EmojiPackSelectionEvent +import com.vitorpamplona.quartz.events.Event +import com.vitorpamplona.quartz.events.GenericRepostEvent +import com.vitorpamplona.quartz.events.GiftWrapEvent import com.vitorpamplona.quartz.events.LnZapEvent +import com.vitorpamplona.quartz.events.LnZapPaymentResponseEvent import com.vitorpamplona.quartz.events.MetadataEvent +import com.vitorpamplona.quartz.events.PeopleListEvent +import com.vitorpamplona.quartz.events.PollNoteEvent import com.vitorpamplona.quartz.events.ReactionEvent import com.vitorpamplona.quartz.events.ReportEvent import com.vitorpamplona.quartz.events.RepostEvent +import com.vitorpamplona.quartz.events.SealedGossipEvent import com.vitorpamplona.quartz.events.TextNoteEvent object NostrAccountDataSource : NostrDataSource("AccountData") { @@ -180,13 +191,26 @@ object NostrAccountDataSource : NostrDataSource("AccountData") { super.auth(relay, challenge) if (this::account.isInitialized) { - val event = account.createAuthEvent(relay, challenge) + val context = Amethyst.instance + val isAmberInstalled = PackageUtils.isAmberInstalled(context) + val event = account.createAuthEvent(relay, challenge, isAmberInstalled) - if (event != null) { - Client.send( - event, - relay.url - ) + if (isAmberInstalled && !account.isWriteable()) { + if (event != null) { + openAmber( + event.toJson(), + SignerType.SIGN_EVENT, + IntentUtils.authActivityResultLauncher, + "" + ) + } + } else { + if (event != null) { + Client.send( + event, + relay.url + ) + } } } } 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 6b179794cf..014a2b4d7a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrLnZapPaymentResponseDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrLnZapPaymentResponseDataSource.kt @@ -44,7 +44,7 @@ class NostrLnZapPaymentResponseDataSource( override fun auth(relay: Relay, challenge: String) { super.auth(relay, challenge) - val event = RelayAuthEvent.create(relay.url, challenge, authSigningKey) + val event = RelayAuthEvent.create(relay.url, challenge, "", authSigningKey) Client.send( event, relay.url diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/PackageUtils.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/PackageUtils.kt index f6dd82972c..1f634e2363 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/PackageUtils.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/PackageUtils.kt @@ -12,6 +12,6 @@ object PackageUtils { } fun isAmberInstalled(context: Context): Boolean { - return isPackageInstalled(context, "com.greenart7c3.nostrsigner") + return isPackageInstalled(context, "com.greenart7c3.nostrsigner.debug") } } 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 3fb7f7c3fe..dd677196ed 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 @@ -1,10 +1,18 @@ package com.vitorpamplona.amethyst.service.notifications +import android.app.Activity +import android.content.Context +import android.content.ContextWrapper import android.util.Log import com.vitorpamplona.amethyst.AccountInfo +import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.service.HttpClient +import com.vitorpamplona.amethyst.service.IntentUtils +import com.vitorpamplona.amethyst.service.PackageUtils +import com.vitorpamplona.amethyst.ui.actions.SignerType +import com.vitorpamplona.amethyst.ui.actions.openAmber import com.vitorpamplona.quartz.events.RelayAuthEvent import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -21,14 +29,16 @@ class RegisterAccounts( // creates proof that it controls all accounts private fun signEventsToProveControlOfAccounts( accounts: List, - notificationToken: String + notificationToken: String, + isAmberInstalled: Boolean ): List { return accounts.mapNotNull { val acc = LocalPreferences.loadFromEncryptedStorage(it.npub) if (acc != null) { val relayToUse = acc.activeRelays()?.firstOrNull { it.read } if (relayToUse != null) { - acc.createAuthEvent(relayToUse, notificationToken) + val event = acc.createAuthEvent(relayToUse, notificationToken, isAmberInstalled) + event } else { null } @@ -38,7 +48,7 @@ class RegisterAccounts( } } - private fun postRegistrationEvent(events: List) { + fun postRegistrationEvent(events: List) { try { val jsonObject = """{ "events": [ ${events.joinToString(", ") { it.toJson() }} ] @@ -64,12 +74,35 @@ class RegisterAccounts( } } + tailrec fun Context.activity(): Activity? = when { + this is Activity -> this + else -> (this as? ContextWrapper)?.baseContext?.activity() + } + fun go(notificationToken: String) { + val isAmberInstalled = PackageUtils.isAmberInstalled(Amethyst.instance) val scope = CoroutineScope(Job() + Dispatchers.IO) scope.launch { - postRegistrationEvent( - signEventsToProveControlOfAccounts(accounts, notificationToken) - ) + Log.d("auth event", "entered") + val accountsWithoutPrivKey = accounts.filter { !it.hasPrivKey } + val accountsWithPrivKey = accounts.filter { it.hasPrivKey } + accountsWithoutPrivKey.forEach { account -> + val events = signEventsToProveControlOfAccounts(listOf(account), notificationToken, isAmberInstalled) + if (events.isNotEmpty()) { + openAmber( + events.first().toJson(), + SignerType.SIGN_EVENT, + IntentUtils.activityResultLauncher, + "" + ) + } + } + + if (accountsWithPrivKey.isNotEmpty()) { + postRegistrationEvent( + signEventsToProveControlOfAccounts(accountsWithPrivKey, notificationToken, isAmberInstalled) + ) + } } } } 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 ef452f774d..4db478e796 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt @@ -1,5 +1,6 @@ package com.vitorpamplona.amethyst.ui +import android.app.Activity import android.content.Context import android.content.Intent import android.net.ConnectivityManager @@ -9,6 +10,7 @@ import android.net.NetworkRequest import android.os.Build import android.os.Bundle import android.util.Log +import android.widget.Toast import androidx.activity.compose.setContent import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.RequiresApi @@ -20,10 +22,14 @@ import androidx.compose.material.Surface import androidx.compose.ui.Modifier import androidx.core.os.LocaleListCompat import androidx.lifecycle.viewmodel.compose.viewModel +import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.ServiceManager +import com.vitorpamplona.amethyst.service.IntentUtils import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus import com.vitorpamplona.amethyst.service.notifications.PushNotificationUtils +import com.vitorpamplona.amethyst.service.notifications.RegisterAccounts +import com.vitorpamplona.amethyst.service.relays.Client import com.vitorpamplona.amethyst.ui.components.DefaultMutedSetting import com.vitorpamplona.amethyst.ui.components.keepPlayingMutex import com.vitorpamplona.amethyst.ui.navigation.Route @@ -38,8 +44,10 @@ import com.vitorpamplona.quartz.events.ChannelCreateEvent import com.vitorpamplona.quartz.events.ChannelMessageEvent import com.vitorpamplona.quartz.events.ChannelMetadataEvent import com.vitorpamplona.quartz.events.CommunityDefinitionEvent +import com.vitorpamplona.quartz.events.Event import com.vitorpamplona.quartz.events.LiveActivitiesEvent import com.vitorpamplona.quartz.events.PrivateDmEvent +import com.vitorpamplona.quartz.events.RelayAuthEvent import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -48,8 +56,60 @@ import java.net.URLEncoder import java.nio.charset.StandardCharsets class MainActivity : AppCompatActivity() { + @OptIn(DelicateCoroutinesApi::class) @RequiresApi(Build.VERSION_CODES.R) override fun onCreate(savedInstanceState: Bundle?) { + IntentUtils.activityResultLauncher = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { + if (it.resultCode != Activity.RESULT_OK) { + GlobalScope.launch(Dispatchers.Main) { + Toast.makeText( + Amethyst.instance, + "Sign request rejected", + Toast.LENGTH_SHORT + ).show() + } + return@registerForActivityResult + } + + val event = it.data?.getStringExtra("event") ?: "" + + val signedEvent = Event.fromJson(event) + val authEvent = RelayAuthEvent(signedEvent.id, signedEvent.pubKey, signedEvent.createdAt, signedEvent.tags, signedEvent.content, signedEvent.sig) + + RegisterAccounts(LocalPreferences.allSavedAccounts()).postRegistrationEvent( + listOf(authEvent) + ) + PushNotificationUtils.hasInit = true + ServiceManager.shouldPauseService = true + } + + IntentUtils.authActivityResultLauncher = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { + if (it.resultCode != Activity.RESULT_OK) { + GlobalScope.launch(Dispatchers.Main) { + Toast.makeText( + Amethyst.instance, + "Sign request rejected", + Toast.LENGTH_SHORT + ).show() + } + return@registerForActivityResult + } + + val event = it.data?.getStringExtra("event") ?: "" + + val signedEvent = Event.fromJson(event) + val authEvent = RelayAuthEvent(signedEvent.id, signedEvent.pubKey, signedEvent.createdAt, signedEvent.tags, signedEvent.content, signedEvent.sig) + + GlobalScope.launch(Dispatchers.IO) { + Client.send(authEvent, authEvent.relay()) + } + ServiceManager.shouldPauseService = true + } + super.onCreate(savedInstanceState) LocalPreferences.migrateSingleUserPrefs() @@ -94,10 +154,12 @@ class MainActivity : AppCompatActivity() { // Only starts after login GlobalScope.launch(Dispatchers.IO) { - ServiceManager.start(this@MainActivity) + if (ServiceManager.shouldPauseService) { + ServiceManager.start(this@MainActivity) + } } - PushNotificationUtils().init(LocalPreferences.allSavedAccounts()) + PushNotificationUtils.init(LocalPreferences.allSavedAccounts()) } override fun onPause() { @@ -105,7 +167,9 @@ class MainActivity : AppCompatActivity() { debugState(this) // } - ServiceManager.pause() + if (ServiceManager.shouldPauseService) { + ServiceManager.pause() + } super.onPause() } @@ -137,8 +201,10 @@ class MainActivity : AppCompatActivity() { Log.d("NETWORKCALLBACK", "onAvailable: Disconnecting and connecting again") // Only starts after login GlobalScope.launch(Dispatchers.IO) { - ServiceManager.pause() - ServiceManager.start(this@MainActivity) + if (ServiceManager.shouldPauseService) { + ServiceManager.pause() + ServiceManager.start(this@MainActivity) + } } } @@ -167,7 +233,9 @@ class MainActivity : AppCompatActivity() { Log.d("NETWORKCALLBACK", "onLost: Disconnecting and pausing relay's connection") // Only starts after login GlobalScope.launch(Dispatchers.IO) { - ServiceManager.pause() + if (ServiceManager.shouldPauseService) { + ServiceManager.pause() + } } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/SignerDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/SignerDialog.kt index b083b0d734..53cd12326f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/SignerDialog.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/SignerDialog.kt @@ -4,9 +4,8 @@ import android.app.Activity.RESULT_OK import android.content.Intent import android.net.Uri import android.widget.Toast -import androidx.activity.compose.ManagedActivityResultLauncher import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.ActivityResult +import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -39,6 +38,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ServiceManager import com.vitorpamplona.amethyst.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer import com.vitorpamplona.quartz.encoders.HexKey @@ -58,9 +58,10 @@ enum class SignerType { fun openAmber( data: String, type: SignerType, - intentResult: ManagedActivityResultLauncher, + intentResult: ActivityResultLauncher, pubKey: HexKey ) { + ServiceManager.shouldPauseService = false val intent = Intent(Intent.ACTION_VIEW, Uri.parse("nostrsigner:$data")) val signerType = when (type) { SignerType.SIGN_EVENT -> "sign_event" @@ -106,6 +107,7 @@ fun SignerDialog( signature ) } + ServiceManager.shouldPauseService = true } ) diff --git a/app/src/play/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt b/app/src/play/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt index 9fc979cb9e..a00c2edd40 100644 --- a/app/src/play/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt +++ b/app/src/play/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt @@ -9,8 +9,12 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch -class PushNotificationUtils { +object PushNotificationUtils { + var hasInit: Boolean = false fun init(accounts: List) { + if (hasInit) { + return + } val scope = CoroutineScope(Job() + Dispatchers.IO) scope.launch { // get user notification token provided by firebase 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 a1675dd37d..d0a3c5beee 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/RelayAuthEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/RelayAuthEvent.kt @@ -21,16 +21,15 @@ class RelayAuthEvent( companion object { const val kind = 22242 - fun create(relay: String, challenge: String, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): RelayAuthEvent { + fun create(relay: String, challenge: String, pubKey: HexKey, privateKey: ByteArray?, createdAt: Long = TimeUtils.now()): RelayAuthEvent { val content = "" - val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey() val tags = listOf( listOf("relay", relay), listOf("challenge", challenge) ) val id = generateId(pubKey, createdAt, kind, tags, content) - val sig = CryptoUtils.sign(id, privateKey) - return RelayAuthEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) + val sig = if (privateKey == null) null else CryptoUtils.sign(id, privateKey) + return RelayAuthEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig?.toHexKey() ?: "") } } }