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
class PushNotificationUtils {
object PushNotificationUtils {
var hasInit: Boolean = true
fun init(accounts: List<AccountInfo>) {
}
}
+1
View File
@@ -4,6 +4,7 @@
<queries>
<package android:name="org.torproject.android"/>
<package android:name="com.greenart7c3.nostrsigner"/>
<package android:name="com.greenart7c3.nostrsigner.debug"/>
</queries>
<!-- Doesn't require a camera -->
@@ -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() {
@@ -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) {
@@ -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
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
)
}
}
}
}
@@ -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
@@ -12,6 +12,6 @@ object PackageUtils {
}
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
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<AccountInfo>,
notificationToken: String
notificationToken: String,
isAmberInstalled: Boolean
): List<RelayAuthEvent> {
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<RelayAuthEvent>) {
fun postRegistrationEvent(events: List<RelayAuthEvent>) {
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)
)
}
}
}
}
@@ -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()
}
}
}
}
@@ -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<Intent, ActivityResult>,
intentResult: ActivityResultLauncher<Intent>,
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
}
)
@@ -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<AccountInfo>) {
if (hasInit) {
return
}
val scope = CoroutineScope(Job() + Dispatchers.IO)
scope.launch {
// get user notification token provided by firebase
@@ -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() ?: "")
}
}
}