From 0f066d08170ab0a10a338a1c128e64941db7bb85 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 24 Jun 2024 17:56:24 -0400 Subject: [PATCH] Saves a copy of the NIP65 and NIP17 relay lists locally --- .../amethyst/LocalPreferences.kt | 83 ++++++++++++-- .../vitorpamplona/amethyst/model/Account.kt | 104 ++++++++++++++--- .../service/notifications/RegisterAccounts.kt | 106 +++++++++++++----- 3 files changed, 247 insertions(+), 46 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index 7a3c1ac78..39d36c3d0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -42,6 +42,8 @@ import com.vitorpamplona.quartz.encoders.Nip47WalletConnect import com.vitorpamplona.quartz.encoders.hexToByteArray import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.encoders.toNpub +import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent +import com.vitorpamplona.quartz.events.ChatMessageRelayListEvent import com.vitorpamplona.quartz.events.ContactListEvent import com.vitorpamplona.quartz.events.Event import com.vitorpamplona.quartz.events.LnZapEvent @@ -90,6 +92,8 @@ private object PrefKeys { const val DEFAULT_DISCOVERY_FOLLOW_LIST = "defaultDiscoveryFollowList" const val ZAP_PAYMENT_REQUEST_SERVER = "zapPaymentServer" const val LATEST_CONTACT_LIST = "latestContactList" + const val LATEST_DM_RELAY_LIST = "latestDMRelayList" + const val LATEST_NIP65_RELAY_LIST = "latestNIP65RelayList" const val HIDE_DELETE_REQUEST_DIALOG = "hide_delete_request_dialog" const val HIDE_BLOCK_ALERT_DIALOG = "hide_block_alert_dialog" const val HIDE_NIP_17_WARNING_DIALOG = "hide_nip24_warning_dialog" // delete later @@ -311,10 +315,33 @@ object LocalPreferences { PrefKeys.ZAP_PAYMENT_REQUEST_SERVER, Event.mapper.writeValueAsString(account.zapPaymentRequest), ) - putString( - PrefKeys.LATEST_CONTACT_LIST, - Event.mapper.writeValueAsString(account.backupContactList), - ) + if (account.backupContactList != null) { + putString( + PrefKeys.LATEST_CONTACT_LIST, + Event.mapper.writeValueAsString(account.backupContactList), + ) + } else { + remove(PrefKeys.LATEST_CONTACT_LIST) + } + + if (account.backupDMRelayList != null) { + putString( + PrefKeys.LATEST_DM_RELAY_LIST, + Event.mapper.writeValueAsString(account.backupDMRelayList), + ) + } else { + remove(PrefKeys.LATEST_DM_RELAY_LIST) + } + + if (account.backupNIP65RelayList != null) { + putString( + PrefKeys.LATEST_NIP65_RELAY_LIST, + Event.mapper.writeValueAsString(account.backupNIP65RelayList), + ) + } else { + remove(PrefKeys.LATEST_NIP65_RELAY_LIST) + } + putBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, account.hideDeleteRequestDialog) putBoolean(PrefKeys.HIDE_NIP_17_WARNING_DIALOG, account.hideNIP17WarningDialog) putBoolean(PrefKeys.HIDE_BLOCK_ALERT_DIALOG, account.hideBlockAlertDialog) @@ -471,8 +498,8 @@ object LocalPreferences { val latestContactList = try { getString(PrefKeys.LATEST_CONTACT_LIST, null)?.let { - println("Decoding Contact List: " + it) - if (it != null) { + if (it != "null") { + println("Decoding Contact List: $it") Event.fromJson(it) as ContactListEvent? } else { null @@ -488,6 +515,46 @@ object LocalPreferences { null } + val latestDmRelayList = + try { + getString(PrefKeys.LATEST_DM_RELAY_LIST, null)?.let { + if (it != "null") { + println("Decoding DM Relay List: $it") + Event.fromJson(it) as ChatMessageRelayListEvent? + } else { + null + } + } + } catch (e: Throwable) { + if (e is CancellationException) throw e + Log.w( + "LocalPreferences", + "Error Decoding DM Relay List ${getString(PrefKeys.LATEST_DM_RELAY_LIST, null)}", + e, + ) + null + } + + val latestNip65RelayList = + try { + getString(PrefKeys.LATEST_NIP65_RELAY_LIST, null)?.let { + if (it != "null") { + println("Decoding NIP65 Relay List: $it") + Event.fromJson(it) as AdvertisedRelayListEvent? + } else { + null + } + } + } catch (e: Throwable) { + if (e is CancellationException) throw e + Log.w( + "LocalPreferences", + "Error Decoding NIP65 Relay List ${getString(PrefKeys.LATEST_NIP65_RELAY_LIST, null)}", + e, + ) + null + } + val pendingAttestations = try { getString(PrefKeys.PENDING_ATTESTATIONS, null)?.let { @@ -502,7 +569,7 @@ object LocalPreferences { if (e is CancellationException) throw e Log.w( "LocalPreferences", - "Error Decoding Contact List ${getString(PrefKeys.LATEST_CONTACT_LIST, null)}", + "Error Decoding Contact List ${getString(PrefKeys.PENDING_ATTESTATIONS, null)}", e, ) null @@ -595,6 +662,8 @@ object LocalPreferences { hideBlockAlertDialog = hideBlockAlertDialog, hideNIP17WarningDialog = hideNIP17WarningDialog, backupContactList = latestContactList, + backupNIP65RelayList = latestNip65RelayList, + backupDMRelayList = latestDmRelayList, proxy = proxy, proxyPort = proxyPort, showSensitiveContent = MutableStateFlow(showSensitiveContent), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 1e3ff69ff..2fd531131 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -116,6 +116,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.combineTransform import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flattenMerge @@ -192,6 +193,8 @@ class Account( var hideBlockAlertDialog: Boolean = false, var hideNIP17WarningDialog: Boolean = false, var backupContactList: ContactListEvent? = null, + var backupDMRelayList: ChatMessageRelayListEvent? = null, + var backupNIP65RelayList: AdvertisedRelayListEvent? = null, var proxy: Proxy? = null, var proxyPort: Int = 9050, var showSensitiveContent: MutableStateFlow = MutableStateFlow(null), @@ -269,7 +272,9 @@ class Account( if (mappedRelaySet.none { it.url == newUrl }) { mappedRelaySet = mappedRelaySet + RelaySetupInfo( - newUrl, true, true, + newUrl, + true, + true, setOf( FeedType.PRIVATE_DMS, ), @@ -294,7 +299,9 @@ class Account( if (mappedRelaySet.none { it.url == newUrl }) { mappedRelaySet = mappedRelaySet + RelaySetupInfo( - newUrl, true, false, + newUrl, + true, + false, setOf( FeedType.SEARCH, ), @@ -319,9 +326,14 @@ class Account( if (mappedRelaySet.none { it.url == newUrl }) { mappedRelaySet = mappedRelaySet + RelaySetupInfo( - newUrl, true, true, + newUrl, + true, + true, setOf( - FeedType.FOLLOWS, FeedType.PUBLIC_CHATS, FeedType.GLOBAL, FeedType.PRIVATE_DMS, + FeedType.FOLLOWS, + FeedType.PUBLIC_CHATS, + FeedType.GLOBAL, + FeedType.PRIVATE_DMS, ), ) } @@ -344,9 +356,14 @@ class Account( if (mappedRelaySet.none { it.url == newUrl }) { mappedRelaySet = mappedRelaySet + RelaySetupInfo( - newUrl, true, true, + newUrl, + true, + true, setOf( - FeedType.FOLLOWS, FeedType.PUBLIC_CHATS, FeedType.GLOBAL, FeedType.PRIVATE_DMS, + FeedType.FOLLOWS, + FeedType.PUBLIC_CHATS, + FeedType.GLOBAL, + FeedType.PRIVATE_DMS, ), ) } @@ -363,10 +380,14 @@ class Account( val write = nip65setup.type == AdvertisedRelayListEvent.AdvertisedRelayType.BOTH || nip65setup.type == AdvertisedRelayListEvent.AdvertisedRelayType.READ RelaySetupInfo( - relay.url, true, relay.write || write, + relay.url, + true, + relay.write || write, relay.feedTypes + setOf( - FeedType.FOLLOWS, FeedType.GLOBAL, FeedType.PUBLIC_CHATS, + FeedType.FOLLOWS, + FeedType.GLOBAL, + FeedType.PUBLIC_CHATS, ), ) } else { @@ -380,9 +401,12 @@ class Account( mappedRelaySet = mappedRelaySet + RelaySetupInfo( - newNip65Setup.relayUrl, true, write, + newNip65Setup.relayUrl, + true, + write, setOf( - FeedType.FOLLOWS, FeedType.PUBLIC_CHATS, + FeedType.FOLLOWS, + FeedType.PUBLIC_CHATS, ), ) } @@ -2635,6 +2659,26 @@ class Account( } } + private fun updateDMRelayList(newDMRelayList: ChatMessageRelayListEvent?) { + if (newDMRelayList == null || newDMRelayList.tags.isEmpty()) return + + // Events might be different objects, we have to compare their ids. + if (backupDMRelayList?.id != newDMRelayList.id) { + backupDMRelayList = newDMRelayList + saveable.invalidateData() + } + } + + private fun updateNIP65RelayList(newNIP65RelayList: AdvertisedRelayListEvent?) { + if (newNIP65RelayList == null || newNIP65RelayList.tags.isEmpty()) return + + // Events might be different objects, we have to compare their ids. + if (backupNIP65RelayList?.id != newNIP65RelayList.id) { + backupNIP65RelayList = newNIP65RelayList + saveable.invalidateData() + } + } + // Takes a User's relay list and adds the types of feeds they are active for. fun activeRelays(): Array? { val usersRelayList = @@ -2968,8 +3012,32 @@ class Account( suspend fun registerObservers() = withContext(Dispatchers.Main) { // saves contact list for the next time. - userProfile().live().follows.observeForever { - GlobalScope.launch(Dispatchers.IO) { updateContactListTo(userProfile().latestContactList) } + scope.launch { + Log.d("AccountRegisterObservers", "Kind 3 Collector Start") + userProfile().flow().follows.stateFlow.collect { + Log.d("AccountRegisterObservers", "Updating Kind 3 ${userProfile().toBestDisplayName()}") + updateContactListTo(userProfile().latestContactList) + } + } + + scope.launch { + Log.d("AccountRegisterObservers", "NIP-17 Relay List Collector Start") + getDMRelayListFlow().collect { + Log.d("AccountRegisterObservers", "Updating DM Relay List for ${userProfile().toBestDisplayName()}") + (it.note.event as? ChatMessageRelayListEvent)?.let { + updateDMRelayList(it) + } + } + } + + scope.launch { + Log.d("AccountRegisterObservers", "NIP-65 Relay List Collector Start") + getNIP65RelayListFlow().collect { + Log.d("AccountRegisterObservers", "Updating NIP-65 List for ${userProfile().toBestDisplayName()}") + (it.note.event as? AdvertisedRelayListEvent)?.let { + updateNIP65RelayList(it) + } + } } // imports transient blocks due to spam. @@ -2990,7 +3058,7 @@ class Account( } init { - Log.d("Init", "Account") + Log.d("Account", "Init") backupContactList?.let { println("Loading saved contacts ${it.toJson()}") @@ -2998,6 +3066,16 @@ class Account( GlobalScope.launch(Dispatchers.IO) { LocalCache.consume(it) } } } + + backupDMRelayList?.let { + println("Loading DM Relay List ${it.toJson()}") + GlobalScope.launch(Dispatchers.IO) { LocalCache.verifyAndConsume(it, null) } + } + + backupNIP65RelayList?.let { + println("Loading saved contacts ${it.toJson()}") + GlobalScope.launch(Dispatchers.IO) { LocalCache.verifyAndConsume(it, null) } + } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt index 6ce76450b..dc6b7b90c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt @@ -25,19 +25,35 @@ import com.vitorpamplona.amethyst.AccountInfo import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.ammolite.service.HttpClientManager +import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent +import com.vitorpamplona.quartz.events.ChatMessageRelayListEvent import com.vitorpamplona.quartz.events.RelayAuthEvent import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.joinAll +import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeoutOrNull import okhttp3.MediaType.Companion.toMediaType import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody +import kotlin.coroutines.resume class RegisterAccounts( private val accounts: List, ) { - private fun recursiveAuthCreation( + val tag = + if (BuildConfig.FLAVOR == "play") { + "RegisterAccounts FirebaseMsgService" + } else { + "RegisterAccounts UnifiedPushService" + } + + private suspend fun signAllAuths( notificationToken: String, remainingTos: List>, output: MutableList, @@ -48,12 +64,32 @@ class RegisterAccounts( return } - val next = remainingTos.first() + coroutineScope { + val jobs = + remainingTos.map { accountRelayPair -> + async { + val result = + withTimeoutOrNull(10000) { + suspendCancellableCoroutine { continuation -> + accountRelayPair.first.createAuthEvent(accountRelayPair.second, notificationToken) { result -> + continuation.resume(result) + } + } + } - next.first.createAuthEvent(next.second, notificationToken) { - output.add(it) - recursiveAuthCreation(notificationToken, remainingTos.filter { next != it }, output, onReady) + if (result != null) { + output.add(result) + } + } + } + + // runs in parallel to avoid overcrowding Amber. + withTimeoutOrNull(15000) { + jobs.joinAll() + } } + + onReady(output) } // creates proof that it controls all accounts @@ -63,25 +99,48 @@ class RegisterAccounts( onReady: (List) -> Unit, ) { val readyToSend = - accounts.mapNotNull { - val acc = LocalPreferences.loadCurrentAccountFromEncryptedStorage(it.npub) - if (acc != null && acc.isWriteable()) { - val readRelays = - acc.userProfile().latestContactList?.relays() ?: acc.backupContactList?.relays() + accounts + .map { + Log.d(tag, "Register Account ${it.npub}") - val relayToUse = readRelays?.firstNotNullOfOrNull { if (it.value.read) it.key else null } - if (relayToUse != null) { - Pair(acc, relayToUse) + val acc = LocalPreferences.loadCurrentAccountFromEncryptedStorage(it.npub) + + if (acc != null && acc.isWriteable()) { + val nip65Read = + ( + LocalCache + .getAddressableNoteIfExists( + AdvertisedRelayListEvent.createAddressTag(acc.userProfile().pubkeyHex), + )?.event as? AdvertisedRelayListEvent + )?.readRelays() ?: acc.backupNIP65RelayList?.readRelays() ?: emptyList() + + Log.d(tag, "Register Account ${acc.userProfile().toBestDisplayName()} NIP65 Reads ${nip65Read.joinToString(", ")}") + + val nip17Read = + ( + LocalCache + .getAddressableNoteIfExists( + ChatMessageRelayListEvent.createAddressTag(acc.userProfile().pubkeyHex), + )?.event as? ChatMessageRelayListEvent + )?.relays() ?: acc.backupDMRelayList?.relays() ?: emptyList() + + Log.d(tag, "Register Account ${acc.userProfile().toBestDisplayName()} NIP17 Reads ${nip17Read.joinToString(", ")}") + + val kind3Relays = (acc.userProfile().latestContactList?.relays() ?: acc.backupContactList?.relays()) + val readKind3Relays = kind3Relays?.mapNotNull { if (it.value.read) it.key else null } ?: emptyList() + + Log.d(tag, "Register Account ${acc.userProfile().toBestDisplayName()} Kind3 Reads ${readKind3Relays.joinToString(", ")}") + + (nip65Read + nip17Read + readKind3Relays).map { + Pair(acc, it) + } } else { - null + emptyList>() } - } else { - null - } - } + }.flatten() val listOfAuthEvents = mutableListOf() - recursiveAuthCreation( + signAllAuths( notificationToken, readyToSend, listOfAuthEvents, @@ -101,7 +160,8 @@ class RegisterAccounts( val body = jsonObject.toRequestBody(mediaType) val request = - Request.Builder() + Request + .Builder() .header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}") .url("https://push.amethyst.social/register") .post(body) @@ -112,12 +172,6 @@ class RegisterAccounts( val isSucess = client.newCall(request).execute().use { it.isSuccessful } } catch (e: java.lang.Exception) { if (e is CancellationException) throw e - val tag = - if (BuildConfig.FLAVOR == "play") { - "FirebaseMsgService" - } else { - "UnifiedPushService" - } Log.e(tag, "Unable to register with push server", e) } }