Saves a copy of the NIP65 and NIP17 relay lists locally

This commit is contained in:
Vitor Pamplona
2024-06-24 17:56:24 -04:00
parent 8cef7bcb75
commit 0f066d0817
3 changed files with 247 additions and 46 deletions
@@ -42,6 +42,8 @@ import com.vitorpamplona.quartz.encoders.Nip47WalletConnect
import com.vitorpamplona.quartz.encoders.hexToByteArray import com.vitorpamplona.quartz.encoders.hexToByteArray
import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.encoders.toNpub 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.ContactListEvent
import com.vitorpamplona.quartz.events.Event import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.LnZapEvent import com.vitorpamplona.quartz.events.LnZapEvent
@@ -90,6 +92,8 @@ private object PrefKeys {
const val DEFAULT_DISCOVERY_FOLLOW_LIST = "defaultDiscoveryFollowList" const val DEFAULT_DISCOVERY_FOLLOW_LIST = "defaultDiscoveryFollowList"
const val ZAP_PAYMENT_REQUEST_SERVER = "zapPaymentServer" const val ZAP_PAYMENT_REQUEST_SERVER = "zapPaymentServer"
const val LATEST_CONTACT_LIST = "latestContactList" 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_DELETE_REQUEST_DIALOG = "hide_delete_request_dialog"
const val HIDE_BLOCK_ALERT_DIALOG = "hide_block_alert_dialog" const val HIDE_BLOCK_ALERT_DIALOG = "hide_block_alert_dialog"
const val HIDE_NIP_17_WARNING_DIALOG = "hide_nip24_warning_dialog" // delete later const val HIDE_NIP_17_WARNING_DIALOG = "hide_nip24_warning_dialog" // delete later
@@ -311,10 +315,33 @@ object LocalPreferences {
PrefKeys.ZAP_PAYMENT_REQUEST_SERVER, PrefKeys.ZAP_PAYMENT_REQUEST_SERVER,
Event.mapper.writeValueAsString(account.zapPaymentRequest), Event.mapper.writeValueAsString(account.zapPaymentRequest),
) )
if (account.backupContactList != null) {
putString( putString(
PrefKeys.LATEST_CONTACT_LIST, PrefKeys.LATEST_CONTACT_LIST,
Event.mapper.writeValueAsString(account.backupContactList), 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_DELETE_REQUEST_DIALOG, account.hideDeleteRequestDialog)
putBoolean(PrefKeys.HIDE_NIP_17_WARNING_DIALOG, account.hideNIP17WarningDialog) putBoolean(PrefKeys.HIDE_NIP_17_WARNING_DIALOG, account.hideNIP17WarningDialog)
putBoolean(PrefKeys.HIDE_BLOCK_ALERT_DIALOG, account.hideBlockAlertDialog) putBoolean(PrefKeys.HIDE_BLOCK_ALERT_DIALOG, account.hideBlockAlertDialog)
@@ -471,8 +498,8 @@ object LocalPreferences {
val latestContactList = val latestContactList =
try { try {
getString(PrefKeys.LATEST_CONTACT_LIST, null)?.let { 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? Event.fromJson(it) as ContactListEvent?
} else { } else {
null null
@@ -488,6 +515,46 @@ object LocalPreferences {
null 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 = val pendingAttestations =
try { try {
getString(PrefKeys.PENDING_ATTESTATIONS, null)?.let { getString(PrefKeys.PENDING_ATTESTATIONS, null)?.let {
@@ -502,7 +569,7 @@ object LocalPreferences {
if (e is CancellationException) throw e if (e is CancellationException) throw e
Log.w( Log.w(
"LocalPreferences", "LocalPreferences",
"Error Decoding Contact List ${getString(PrefKeys.LATEST_CONTACT_LIST, null)}", "Error Decoding Contact List ${getString(PrefKeys.PENDING_ATTESTATIONS, null)}",
e, e,
) )
null null
@@ -595,6 +662,8 @@ object LocalPreferences {
hideBlockAlertDialog = hideBlockAlertDialog, hideBlockAlertDialog = hideBlockAlertDialog,
hideNIP17WarningDialog = hideNIP17WarningDialog, hideNIP17WarningDialog = hideNIP17WarningDialog,
backupContactList = latestContactList, backupContactList = latestContactList,
backupNIP65RelayList = latestNip65RelayList,
backupDMRelayList = latestDmRelayList,
proxy = proxy, proxy = proxy,
proxyPort = proxyPort, proxyPort = proxyPort,
showSensitiveContent = MutableStateFlow(showSensitiveContent), showSensitiveContent = MutableStateFlow(showSensitiveContent),
@@ -116,6 +116,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combineTransform import kotlinx.coroutines.flow.combineTransform
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flattenMerge import kotlinx.coroutines.flow.flattenMerge
@@ -192,6 +193,8 @@ class Account(
var hideBlockAlertDialog: Boolean = false, var hideBlockAlertDialog: Boolean = false,
var hideNIP17WarningDialog: Boolean = false, var hideNIP17WarningDialog: Boolean = false,
var backupContactList: ContactListEvent? = null, var backupContactList: ContactListEvent? = null,
var backupDMRelayList: ChatMessageRelayListEvent? = null,
var backupNIP65RelayList: AdvertisedRelayListEvent? = null,
var proxy: Proxy? = null, var proxy: Proxy? = null,
var proxyPort: Int = 9050, var proxyPort: Int = 9050,
var showSensitiveContent: MutableStateFlow<Boolean?> = MutableStateFlow(null), var showSensitiveContent: MutableStateFlow<Boolean?> = MutableStateFlow(null),
@@ -269,7 +272,9 @@ class Account(
if (mappedRelaySet.none { it.url == newUrl }) { if (mappedRelaySet.none { it.url == newUrl }) {
mappedRelaySet = mappedRelaySet + mappedRelaySet = mappedRelaySet +
RelaySetupInfo( RelaySetupInfo(
newUrl, true, true, newUrl,
true,
true,
setOf( setOf(
FeedType.PRIVATE_DMS, FeedType.PRIVATE_DMS,
), ),
@@ -294,7 +299,9 @@ class Account(
if (mappedRelaySet.none { it.url == newUrl }) { if (mappedRelaySet.none { it.url == newUrl }) {
mappedRelaySet = mappedRelaySet + mappedRelaySet = mappedRelaySet +
RelaySetupInfo( RelaySetupInfo(
newUrl, true, false, newUrl,
true,
false,
setOf( setOf(
FeedType.SEARCH, FeedType.SEARCH,
), ),
@@ -319,9 +326,14 @@ class Account(
if (mappedRelaySet.none { it.url == newUrl }) { if (mappedRelaySet.none { it.url == newUrl }) {
mappedRelaySet = mappedRelaySet + mappedRelaySet = mappedRelaySet +
RelaySetupInfo( RelaySetupInfo(
newUrl, true, true, newUrl,
true,
true,
setOf( 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 }) { if (mappedRelaySet.none { it.url == newUrl }) {
mappedRelaySet = mappedRelaySet + mappedRelaySet = mappedRelaySet +
RelaySetupInfo( RelaySetupInfo(
newUrl, true, true, newUrl,
true,
true,
setOf( 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 val write = nip65setup.type == AdvertisedRelayListEvent.AdvertisedRelayType.BOTH || nip65setup.type == AdvertisedRelayListEvent.AdvertisedRelayType.READ
RelaySetupInfo( RelaySetupInfo(
relay.url, true, relay.write || write, relay.url,
true,
relay.write || write,
relay.feedTypes + relay.feedTypes +
setOf( setOf(
FeedType.FOLLOWS, FeedType.GLOBAL, FeedType.PUBLIC_CHATS, FeedType.FOLLOWS,
FeedType.GLOBAL,
FeedType.PUBLIC_CHATS,
), ),
) )
} else { } else {
@@ -380,9 +401,12 @@ class Account(
mappedRelaySet = mappedRelaySet + mappedRelaySet = mappedRelaySet +
RelaySetupInfo( RelaySetupInfo(
newNip65Setup.relayUrl, true, write, newNip65Setup.relayUrl,
true,
write,
setOf( 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. // Takes a User's relay list and adds the types of feeds they are active for.
fun activeRelays(): Array<RelaySetupInfo>? { fun activeRelays(): Array<RelaySetupInfo>? {
val usersRelayList = val usersRelayList =
@@ -2968,8 +3012,32 @@ class Account(
suspend fun registerObservers() = suspend fun registerObservers() =
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
// saves contact list for the next time. // saves contact list for the next time.
userProfile().live().follows.observeForever { scope.launch {
GlobalScope.launch(Dispatchers.IO) { updateContactListTo(userProfile().latestContactList) } 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. // imports transient blocks due to spam.
@@ -2990,7 +3058,7 @@ class Account(
} }
init { init {
Log.d("Init", "Account") Log.d("Account", "Init")
backupContactList?.let { backupContactList?.let {
println("Loading saved contacts ${it.toJson()}") println("Loading saved contacts ${it.toJson()}")
@@ -2998,6 +3066,16 @@ class Account(
GlobalScope.launch(Dispatchers.IO) { LocalCache.consume(it) } 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) }
}
} }
} }
@@ -25,19 +25,35 @@ import com.vitorpamplona.amethyst.AccountInfo
import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.ammolite.service.HttpClientManager 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 com.vitorpamplona.quartz.events.RelayAuthEvent
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers 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.withContext
import kotlinx.coroutines.withTimeoutOrNull
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import kotlin.coroutines.resume
class RegisterAccounts( class RegisterAccounts(
private val accounts: List<AccountInfo>, private val accounts: List<AccountInfo>,
) { ) {
private fun recursiveAuthCreation( val tag =
if (BuildConfig.FLAVOR == "play") {
"RegisterAccounts FirebaseMsgService"
} else {
"RegisterAccounts UnifiedPushService"
}
private suspend fun signAllAuths(
notificationToken: String, notificationToken: String,
remainingTos: List<Pair<Account, String>>, remainingTos: List<Pair<Account, String>>,
output: MutableList<RelayAuthEvent>, output: MutableList<RelayAuthEvent>,
@@ -48,13 +64,33 @@ class RegisterAccounts(
return return
} }
val next = remainingTos.first() coroutineScope {
val jobs =
next.first.createAuthEvent(next.second, notificationToken) { remainingTos.map { accountRelayPair ->
output.add(it) async {
recursiveAuthCreation(notificationToken, remainingTos.filter { next != it }, output, onReady) val result =
withTimeoutOrNull(10000) {
suspendCancellableCoroutine { continuation ->
accountRelayPair.first.createAuthEvent(accountRelayPair.second, notificationToken) { result ->
continuation.resume(result)
} }
} }
}
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 // creates proof that it controls all accounts
private suspend fun signEventsToProveControlOfAccounts( private suspend fun signEventsToProveControlOfAccounts(
@@ -63,25 +99,48 @@ class RegisterAccounts(
onReady: (List<RelayAuthEvent>) -> Unit, onReady: (List<RelayAuthEvent>) -> Unit,
) { ) {
val readyToSend = val readyToSend =
accounts.mapNotNull { accounts
val acc = LocalPreferences.loadCurrentAccountFromEncryptedStorage(it.npub) .map {
if (acc != null && acc.isWriteable()) { Log.d(tag, "Register Account ${it.npub}")
val readRelays =
acc.userProfile().latestContactList?.relays() ?: acc.backupContactList?.relays()
val relayToUse = readRelays?.firstNotNullOfOrNull { if (it.value.read) it.key else null } val acc = LocalPreferences.loadCurrentAccountFromEncryptedStorage(it.npub)
if (relayToUse != null) {
Pair(acc, relayToUse) if (acc != null && acc.isWriteable()) {
} else { val nip65Read =
null (
LocalCache
.getAddressableNoteIfExists(
AdvertisedRelayListEvent.createAddressTag(acc.userProfile().pubkeyHex),
)?.event as? AdvertisedRelayListEvent
)?.readRelays() ?: acc.backupNIP65RelayList?.readRelays() ?: emptyList<String>()
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<String>()
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<String>()
Log.d(tag, "Register Account ${acc.userProfile().toBestDisplayName()} Kind3 Reads ${readKind3Relays.joinToString(", ")}")
(nip65Read + nip17Read + readKind3Relays).map {
Pair(acc, it)
} }
} else { } else {
null emptyList<Pair<Account, String>>()
}
} }
}.flatten()
val listOfAuthEvents = mutableListOf<RelayAuthEvent>() val listOfAuthEvents = mutableListOf<RelayAuthEvent>()
recursiveAuthCreation( signAllAuths(
notificationToken, notificationToken,
readyToSend, readyToSend,
listOfAuthEvents, listOfAuthEvents,
@@ -101,7 +160,8 @@ class RegisterAccounts(
val body = jsonObject.toRequestBody(mediaType) val body = jsonObject.toRequestBody(mediaType)
val request = val request =
Request.Builder() Request
.Builder()
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}") .header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
.url("https://push.amethyst.social/register") .url("https://push.amethyst.social/register")
.post(body) .post(body)
@@ -112,12 +172,6 @@ class RegisterAccounts(
val isSucess = client.newCall(request).execute().use { it.isSuccessful } val isSucess = client.newCall(request).execute().use { it.isSuccessful }
} catch (e: java.lang.Exception) { } catch (e: java.lang.Exception) {
if (e is CancellationException) throw e 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) Log.e(tag, "Unable to register with push server", e)
} }
} }