implemented amber support for auth event and the notifications

This commit is contained in:
greenart7c3
2023-08-21 14:48:11 -03:00
parent 348e07a307
commit f9272f5cfc
13 changed files with 181 additions and 33 deletions
@@ -2,7 +2,8 @@ package com.vitorpamplona.amethyst.service.notifications
import com.vitorpamplona.amethyst.AccountInfo import com.vitorpamplona.amethyst.AccountInfo
class PushNotificationUtils { object PushNotificationUtils {
var hasInit: Boolean = true
fun init(accounts: List<AccountInfo>) { fun init(accounts: List<AccountInfo>) {
} }
} }
+1
View File
@@ -4,6 +4,7 @@
<queries> <queries>
<package android:name="org.torproject.android"/> <package android:name="org.torproject.android"/>
<package android:name="com.greenart7c3.nostrsigner"/> <package android:name="com.greenart7c3.nostrsigner"/>
<package android:name="com.greenart7c3.nostrsigner.debug"/>
</queries> </queries>
<!-- Doesn't require a camera --> <!-- Doesn't require a camera -->
@@ -34,6 +34,8 @@ import com.vitorpamplona.amethyst.ui.actions.ImageUploader
import java.io.File import java.io.File
object ServiceManager { 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 private var account: Account? = null
fun start(account: Account, context: Context) { fun start(account: Account, context: Context) {
@@ -43,6 +45,9 @@ object ServiceManager {
@Synchronized @Synchronized
fun start(context: Context) { fun start(context: Context) {
if (isStarted && account != null) {
return
}
Log.d("ServiceManager", "Starting Relay Services") Log.d("ServiceManager", "Starting Relay Services")
val myAccount = account val myAccount = account
@@ -87,6 +92,7 @@ object ServiceManager {
NostrSingleEventDataSource.start() NostrSingleEventDataSource.start()
NostrSingleChannelDataSource.start() NostrSingleChannelDataSource.start()
NostrSingleUserDataSource.start() NostrSingleUserDataSource.start()
isStarted = true
} }
} }
@@ -112,6 +118,7 @@ object ServiceManager {
NostrVideoDataSource.stop() NostrVideoDataSource.stop()
Client.disconnect() Client.disconnect()
isStarted = false
} }
fun cleanUp() { fun cleanUp() {
@@ -1354,10 +1354,10 @@ class Account(
LocalCache.consume(event) LocalCache.consume(event)
} }
fun createAuthEvent(relay: Relay, challenge: String): RelayAuthEvent? { fun createAuthEvent(relay: Relay, challenge: String, isAmberInstalled: Boolean): RelayAuthEvent? {
if (!isWriteable()) return null 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) { fun removePublicBookmark(note: Note) {
@@ -0,0 +1,9 @@
package com.vitorpamplona.amethyst.service
import android.content.Intent
import androidx.activity.result.ActivityResultLauncher
object IntentUtils {
lateinit var activityResultLauncher: ActivityResultLauncher<Intent>
lateinit var authActivityResultLauncher: ActivityResultLauncher<Intent>
}
@@ -1,5 +1,6 @@
package com.vitorpamplona.amethyst.service package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES 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.JsonFilter
import com.vitorpamplona.amethyst.service.relays.Relay import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.service.relays.TypedFilter 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.BadgeAwardEvent
import com.vitorpamplona.quartz.events.BadgeProfilesEvent import com.vitorpamplona.quartz.events.BadgeProfilesEvent
import com.vitorpamplona.quartz.events.BookmarkListEvent import com.vitorpamplona.quartz.events.BookmarkListEvent
import com.vitorpamplona.quartz.events.ChannelMessageEvent import com.vitorpamplona.quartz.events.ChannelMessageEvent
import com.vitorpamplona.quartz.events.ContactListEvent 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.LnZapEvent
import com.vitorpamplona.quartz.events.LnZapPaymentResponseEvent
import com.vitorpamplona.quartz.events.MetadataEvent 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.ReactionEvent
import com.vitorpamplona.quartz.events.ReportEvent import com.vitorpamplona.quartz.events.ReportEvent
import com.vitorpamplona.quartz.events.RepostEvent import com.vitorpamplona.quartz.events.RepostEvent
import com.vitorpamplona.quartz.events.SealedGossipEvent
import com.vitorpamplona.quartz.events.TextNoteEvent import com.vitorpamplona.quartz.events.TextNoteEvent
object NostrAccountDataSource : NostrDataSource("AccountData") { object NostrAccountDataSource : NostrDataSource("AccountData") {
@@ -180,13 +191,26 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
super.auth(relay, challenge) super.auth(relay, challenge)
if (this::account.isInitialized) { 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) { if (isAmberInstalled && !account.isWriteable()) {
Client.send( if (event != null) {
event, openAmber(
relay.url event.toJson(),
) SignerType.SIGN_EVENT,
IntentUtils.authActivityResultLauncher,
""
)
}
} else {
if (event != null) {
Client.send(
event,
relay.url
)
}
} }
} }
} }
@@ -44,7 +44,7 @@ class NostrLnZapPaymentResponseDataSource(
override fun auth(relay: Relay, challenge: String) { override fun auth(relay: Relay, challenge: String) {
super.auth(relay, challenge) super.auth(relay, challenge)
val event = RelayAuthEvent.create(relay.url, challenge, authSigningKey) val event = RelayAuthEvent.create(relay.url, challenge, "", authSigningKey)
Client.send( Client.send(
event, event,
relay.url relay.url
@@ -12,6 +12,6 @@ object PackageUtils {
} }
fun isAmberInstalled(context: Context): Boolean { fun isAmberInstalled(context: Context): Boolean {
return isPackageInstalled(context, "com.greenart7c3.nostrsigner") return isPackageInstalled(context, "com.greenart7c3.nostrsigner.debug")
} }
} }
@@ -1,10 +1,18 @@
package com.vitorpamplona.amethyst.service.notifications package com.vitorpamplona.amethyst.service.notifications
import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import android.util.Log import android.util.Log
import com.vitorpamplona.amethyst.AccountInfo import com.vitorpamplona.amethyst.AccountInfo
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.service.HttpClient 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 com.vitorpamplona.quartz.events.RelayAuthEvent
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -21,14 +29,16 @@ class RegisterAccounts(
// creates proof that it controls all accounts // creates proof that it controls all accounts
private fun signEventsToProveControlOfAccounts( private fun signEventsToProveControlOfAccounts(
accounts: List<AccountInfo>, accounts: List<AccountInfo>,
notificationToken: String notificationToken: String,
isAmberInstalled: Boolean
): List<RelayAuthEvent> { ): List<RelayAuthEvent> {
return accounts.mapNotNull { return accounts.mapNotNull {
val acc = LocalPreferences.loadFromEncryptedStorage(it.npub) val acc = LocalPreferences.loadFromEncryptedStorage(it.npub)
if (acc != null) { if (acc != null) {
val relayToUse = acc.activeRelays()?.firstOrNull { it.read } val relayToUse = acc.activeRelays()?.firstOrNull { it.read }
if (relayToUse != null) { if (relayToUse != null) {
acc.createAuthEvent(relayToUse, notificationToken) val event = acc.createAuthEvent(relayToUse, notificationToken, isAmberInstalled)
event
} else { } else {
null null
} }
@@ -38,7 +48,7 @@ class RegisterAccounts(
} }
} }
private fun postRegistrationEvent(events: List<RelayAuthEvent>) { fun postRegistrationEvent(events: List<RelayAuthEvent>) {
try { try {
val jsonObject = """{ val jsonObject = """{
"events": [ ${events.joinToString(", ") { it.toJson() }} ] "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) { fun go(notificationToken: String) {
val isAmberInstalled = PackageUtils.isAmberInstalled(Amethyst.instance)
val scope = CoroutineScope(Job() + Dispatchers.IO) val scope = CoroutineScope(Job() + Dispatchers.IO)
scope.launch { scope.launch {
postRegistrationEvent( Log.d("auth event", "entered")
signEventsToProveControlOfAccounts(accounts, notificationToken) 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)
)
}
} }
} }
} }
@@ -1,5 +1,6 @@
package com.vitorpamplona.amethyst.ui package com.vitorpamplona.amethyst.ui
import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.ConnectivityManager import android.net.ConnectivityManager
@@ -9,6 +10,7 @@ import android.net.NetworkRequest
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.widget.Toast
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
@@ -20,10 +22,14 @@ import androidx.compose.material.Surface
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.core.os.LocaleListCompat import androidx.core.os.LocaleListCompat
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.ServiceManager import com.vitorpamplona.amethyst.ServiceManager
import com.vitorpamplona.amethyst.service.IntentUtils
import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus
import com.vitorpamplona.amethyst.service.notifications.PushNotificationUtils 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.DefaultMutedSetting
import com.vitorpamplona.amethyst.ui.components.keepPlayingMutex import com.vitorpamplona.amethyst.ui.components.keepPlayingMutex
import com.vitorpamplona.amethyst.ui.navigation.Route 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.ChannelMessageEvent
import com.vitorpamplona.quartz.events.ChannelMetadataEvent import com.vitorpamplona.quartz.events.ChannelMetadataEvent
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.LiveActivitiesEvent import com.vitorpamplona.quartz.events.LiveActivitiesEvent
import com.vitorpamplona.quartz.events.PrivateDmEvent import com.vitorpamplona.quartz.events.PrivateDmEvent
import com.vitorpamplona.quartz.events.RelayAuthEvent
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
@@ -48,8 +56,60 @@ import java.net.URLEncoder
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
@OptIn(DelicateCoroutinesApi::class)
@RequiresApi(Build.VERSION_CODES.R) @RequiresApi(Build.VERSION_CODES.R)
override fun onCreate(savedInstanceState: Bundle?) { 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) super.onCreate(savedInstanceState)
LocalPreferences.migrateSingleUserPrefs() LocalPreferences.migrateSingleUserPrefs()
@@ -94,10 +154,12 @@ class MainActivity : AppCompatActivity() {
// Only starts after login // Only starts after login
GlobalScope.launch(Dispatchers.IO) { 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() { override fun onPause() {
@@ -105,7 +167,9 @@ class MainActivity : AppCompatActivity() {
debugState(this) debugState(this)
// } // }
ServiceManager.pause() if (ServiceManager.shouldPauseService) {
ServiceManager.pause()
}
super.onPause() super.onPause()
} }
@@ -137,8 +201,10 @@ class MainActivity : AppCompatActivity() {
Log.d("NETWORKCALLBACK", "onAvailable: Disconnecting and connecting again") Log.d("NETWORKCALLBACK", "onAvailable: Disconnecting and connecting again")
// Only starts after login // Only starts after login
GlobalScope.launch(Dispatchers.IO) { GlobalScope.launch(Dispatchers.IO) {
ServiceManager.pause() if (ServiceManager.shouldPauseService) {
ServiceManager.start(this@MainActivity) ServiceManager.pause()
ServiceManager.start(this@MainActivity)
}
} }
} }
@@ -167,7 +233,9 @@ class MainActivity : AppCompatActivity() {
Log.d("NETWORKCALLBACK", "onLost: Disconnecting and pausing relay's connection") Log.d("NETWORKCALLBACK", "onLost: Disconnecting and pausing relay's connection")
// Only starts after login // Only starts after login
GlobalScope.launch(Dispatchers.IO) { GlobalScope.launch(Dispatchers.IO) {
ServiceManager.pause() if (ServiceManager.shouldPauseService) {
ServiceManager.pause()
}
} }
} }
} }
@@ -4,9 +4,8 @@ import android.app.Activity.RESULT_OK
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.widget.Toast import android.widget.Toast
import androidx.activity.compose.ManagedActivityResultLauncher
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.ActivityResult import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column 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.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ServiceManager
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.HexKey
@@ -58,9 +58,10 @@ enum class SignerType {
fun openAmber( fun openAmber(
data: String, data: String,
type: SignerType, type: SignerType,
intentResult: ManagedActivityResultLauncher<Intent, ActivityResult>, intentResult: ActivityResultLauncher<Intent>,
pubKey: HexKey pubKey: HexKey
) { ) {
ServiceManager.shouldPauseService = false
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("nostrsigner:$data")) val intent = Intent(Intent.ACTION_VIEW, Uri.parse("nostrsigner:$data"))
val signerType = when (type) { val signerType = when (type) {
SignerType.SIGN_EVENT -> "sign_event" SignerType.SIGN_EVENT -> "sign_event"
@@ -106,6 +107,7 @@ fun SignerDialog(
signature signature
) )
} }
ServiceManager.shouldPauseService = true
} }
) )
@@ -9,8 +9,12 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class PushNotificationUtils { object PushNotificationUtils {
var hasInit: Boolean = false
fun init(accounts: List<AccountInfo>) { fun init(accounts: List<AccountInfo>) {
if (hasInit) {
return
}
val scope = CoroutineScope(Job() + Dispatchers.IO) val scope = CoroutineScope(Job() + Dispatchers.IO)
scope.launch { scope.launch {
// get user notification token provided by firebase // get user notification token provided by firebase
@@ -21,16 +21,15 @@ class RelayAuthEvent(
companion object { companion object {
const val kind = 22242 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 content = ""
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val tags = listOf( val tags = listOf(
listOf("relay", relay), listOf("relay", relay),
listOf("challenge", challenge) listOf("challenge", challenge)
) )
val id = generateId(pubKey, createdAt, kind, tags, content) val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = CryptoUtils.sign(id, privateKey) val sig = if (privateKey == null) null else CryptoUtils.sign(id, privateKey)
return RelayAuthEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) return RelayAuthEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig?.toHexKey() ?: "")
} }
} }
} }