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 7c88d8846..eb16da414 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -47,6 +47,7 @@ import com.vitorpamplona.quartz.crypto.KeyPair import com.vitorpamplona.quartz.encoders.ATag import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.Nip47WalletConnect +import com.vitorpamplona.quartz.encoders.RelayUrlFormatter import com.vitorpamplona.quartz.encoders.hexToByteArray import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent @@ -238,10 +239,16 @@ class Account( userProfile().flow().relays.stateFlow, ) { nip65RelayList, dmRelayList, searchRelayList, privateOutBox, userProfile -> val baseRelaySet = activeRelays() ?: convertLocalRelays() - val newDMRelaySet = (dmRelayList.note.event as? ChatMessageRelayListEvent)?.relays()?.toSet() ?: emptySet() - val searchRelaySet = (searchRelayList.note.event as? SearchRelayListEvent)?.relays()?.toSet() ?: Constants.defaultSearchRelaySet - val nip65RelaySet = (nip65RelayList.note.event as? AdvertisedRelayListEvent)?.relays() - val privateOutboxRelaySet = (privateOutBox.note.event as? PrivateOutboxRelayListEvent)?.relays() ?: emptySet() + val newDMRelaySet = (dmRelayList.note.event as? ChatMessageRelayListEvent)?.relays()?.map { RelayUrlFormatter.normalize(it) }?.toSet() ?: emptySet() + val searchRelaySet = (searchRelayList.note.event as? SearchRelayListEvent)?.relays()?.map { RelayUrlFormatter.normalize(it) }?.toSet() ?: Constants.defaultSearchRelaySet + val nip65RelaySet = + (nip65RelayList.note.event as? AdvertisedRelayListEvent)?.relays()?.map { + AdvertisedRelayListEvent.AdvertisedRelayInfo( + RelayUrlFormatter.normalize(it.relayUrl), + it.type, + ) + } + val privateOutboxRelaySet = (privateOutBox.note.event as? PrivateOutboxRelayListEvent)?.relays()?.map { RelayUrlFormatter.normalize(it) }?.toSet() ?: emptySet() // ------ // DMs @@ -2596,22 +2603,24 @@ class Account( fun activeRelays(): Array? { val usersRelayList = userProfile().latestContactList?.relays()?.map { + val url = RelayUrlFormatter.normalize(it.key) + val localFeedTypes = - localRelays.firstOrNull { localRelay -> localRelay.url == it.key }?.feedTypes + localRelays.firstOrNull { localRelay -> RelayUrlFormatter.normalize(localRelay.url) == url }?.feedTypes ?: Constants.defaultRelays - .filter { defaultRelay -> defaultRelay.url == it.key } + .filter { defaultRelay -> defaultRelay.url == url } .firstOrNull() ?.feedTypes ?: FeedType.values().toSet() - Relay(it.key, it.value.read, it.value.write, localFeedTypes) + Relay(url, it.value.read, it.value.write, localFeedTypes) } ?: return null return usersRelayList.toTypedArray() } fun convertLocalRelays(): Array { - return localRelays.map { Relay(it.url, it.read, it.write, it.feedTypes) }.toTypedArray() + return localRelays.map { Relay(RelayUrlFormatter.normalize(it.url), it.read, it.write, it.feedTypes) }.toTypedArray() } fun activeGlobalRelays(): Array { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Constants.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Constants.kt index 8450e65d7..f0d8189b8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Constants.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Constants.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.service.relays import com.vitorpamplona.amethyst.model.RelaySetupInfo +import com.vitorpamplona.quartz.encoders.RelayUrlFormatter object Constants { val activeTypes = setOf(FeedType.FOLLOWS, FeedType.PRIVATE_DMS) @@ -36,21 +37,26 @@ object Constants { val defaultRelays = arrayOf( // Free relays for only DMs, Chats and Follows due to the amount of spam - RelaySetupInfo("wss://nostr.bitcoiner.social", read = true, write = true, feedTypes = activeTypesChats), - RelaySetupInfo("wss://relay.nostr.bg", read = true, write = true, feedTypes = activeTypesChats), - RelaySetupInfo("wss://nostr.oxtr.dev", read = true, write = true, feedTypes = activeTypesChats), - RelaySetupInfo("wss://nostr.fmt.wiz.biz", read = true, write = false, feedTypes = activeTypesChats), - RelaySetupInfo("wss://relay.damus.io", read = true, write = true, feedTypes = activeTypes), + RelaySetupInfo(RelayUrlFormatter.normalize("wss://nostr.bitcoiner.social"), read = true, write = true, feedTypes = activeTypesChats), + RelaySetupInfo(RelayUrlFormatter.normalize("wss://relay.nostr.bg"), read = true, write = true, feedTypes = activeTypesChats), + RelaySetupInfo(RelayUrlFormatter.normalize("wss://nostr.oxtr.dev"), read = true, write = true, feedTypes = activeTypesChats), + RelaySetupInfo(RelayUrlFormatter.normalize("wss://nostr.fmt.wiz.biz"), read = true, write = false, feedTypes = activeTypesChats), + RelaySetupInfo(RelayUrlFormatter.normalize("wss://relay.damus.io"), read = true, write = true, feedTypes = activeTypes), // Global - RelaySetupInfo("wss://nostr.mom", read = true, write = true, feedTypes = activeTypesGlobalChats), - RelaySetupInfo("wss://nos.lol", read = true, write = true, feedTypes = activeTypesGlobalChats), + RelaySetupInfo(RelayUrlFormatter.normalize("wss://nostr.mom"), read = true, write = true, feedTypes = activeTypesGlobalChats), + RelaySetupInfo(RelayUrlFormatter.normalize("wss://nos.lol"), read = true, write = true, feedTypes = activeTypesGlobalChats), // Paid relays - RelaySetupInfo("wss://nostr.wine", read = true, write = false, feedTypes = activeTypesGlobalChats), + RelaySetupInfo(RelayUrlFormatter.normalize("wss://nostr.wine"), read = true, write = false, feedTypes = activeTypesGlobalChats), // Supporting NIP-50 - RelaySetupInfo("wss://relay.nostr.band", read = true, write = false, feedTypes = activeTypesSearch), - RelaySetupInfo("wss://nostr.wine", read = true, write = false, feedTypes = activeTypesSearch), - RelaySetupInfo("wss://relay.noswhere.com", read = true, write = false, feedTypes = activeTypesSearch), + RelaySetupInfo(RelayUrlFormatter.normalize("wss://relay.nostr.band"), read = true, write = false, feedTypes = activeTypesSearch), + RelaySetupInfo(RelayUrlFormatter.normalize("wss://nostr.wine"), read = true, write = false, feedTypes = activeTypesSearch), + RelaySetupInfo(RelayUrlFormatter.normalize("wss://relay.noswhere.com"), read = true, write = false, feedTypes = activeTypesSearch), ) - val defaultSearchRelaySet = setOf("wss://relay.nostr.band", "wss://nostr.wine", "wss://relay.noswhere.com") + val defaultSearchRelaySet = + setOf( + RelayUrlFormatter.normalize("wss://relay.nostr.band"), + RelayUrlFormatter.normalize("wss://nostr.wine"), + RelayUrlFormatter.normalize("wss://relay.noswhere.com"), + ) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/BasicRelaySetupInfoModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/BasicRelaySetupInfoModel.kt index 07a26e7f2..69fb9a41a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/BasicRelaySetupInfoModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/BasicRelaySetupInfoModel.kt @@ -25,6 +25,7 @@ import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.Nip11CachedRetriever import com.vitorpamplona.amethyst.service.relays.RelayStats +import com.vitorpamplona.quartz.encoders.RelayUrlFormatter import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -79,7 +80,7 @@ abstract class BasicRelaySetupInfoModel : ViewModel() { relayList.map { relayUrl -> BasicRelaySetupInfo( - relayUrl, + RelayUrlFormatter.normalize(relayUrl), RelayStats.get(relayUrl), ) }.distinctBy { it.url }.sortedBy { it.relayStat.receivedBytes }.reversed() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/Kind3RelayListViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/Kind3RelayListViewModel.kt index 18ac9e0b4..bbd00140b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/Kind3RelayListViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/Kind3RelayListViewModel.kt @@ -28,6 +28,7 @@ import com.vitorpamplona.amethyst.service.Nip11CachedRetriever import com.vitorpamplona.amethyst.service.relays.Constants import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.RelayStats +import com.vitorpamplona.quartz.encoders.RelayUrlFormatter import kotlinx.collections.immutable.toImmutableSet import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow @@ -101,7 +102,7 @@ class Kind3RelayListViewModel : ViewModel() { ?: FeedType.values().toSet().toImmutableSet() Kind3BasicRelaySetupInfo( - url = it.key, + url = RelayUrlFormatter.normalize(it.key), read = it.value.read, write = it.value.write, feedTypes = localInfoFeedTypes, @@ -115,7 +116,7 @@ class Kind3RelayListViewModel : ViewModel() { account.localRelays .map { Kind3BasicRelaySetupInfo( - url = it.url, + url = RelayUrlFormatter.normalize(it.url), read = it.read, write = it.write, feedTypes = it.feedTypes, @@ -135,7 +136,7 @@ class Kind3RelayListViewModel : ViewModel() { _relays.update { defaultRelays.map { Kind3BasicRelaySetupInfo( - url = it.url, + url = RelayUrlFormatter.normalize(it.url), read = it.read, write = it.write, feedTypes = it.feedTypes, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/Nip65RelayListViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/Nip65RelayListViewModel.kt index fa3f6afdc..615a41e03 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/Nip65RelayListViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/Nip65RelayListViewModel.kt @@ -25,6 +25,7 @@ import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.Nip11CachedRetriever import com.vitorpamplona.amethyst.service.relays.RelayStats +import com.vitorpamplona.quartz.encoders.RelayUrlFormatter import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow @@ -106,7 +107,7 @@ class Nip65RelayListViewModel : ViewModel() { relayList.map { relayUrl -> BasicRelaySetupInfo( - relayUrl, + RelayUrlFormatter.normalize(relayUrl), RelayStats.get(relayUrl), ) }.distinctBy { it.url }.sortedBy { it.relayStat.receivedBytes }.reversed() @@ -117,7 +118,7 @@ class Nip65RelayListViewModel : ViewModel() { relayList.map { relayUrl -> BasicRelaySetupInfo( - relayUrl, + RelayUrlFormatter.normalize(relayUrl), RelayStats.get(relayUrl), ) }.distinctBy { it.url }.sortedBy { it.relayStat.receivedBytes }.reversed() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt index 9b4f2ac9c..9c27ff91d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt @@ -100,6 +100,7 @@ import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.Font14SP import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.quartz.encoders.Nip47WalletConnect +import com.vitorpamplona.quartz.encoders.RelayUrlFormatter import com.vitorpamplona.quartz.encoders.decodePrivateKeyAsHexOrNull import com.vitorpamplona.quartz.encoders.decodePublicKey import com.vitorpamplona.quartz.encoders.toHexKey @@ -166,18 +167,12 @@ class UpdateZapAmountViewModel(val account: Account) : ViewModel() { val relayUrl = walletConnectRelay.text .ifBlank { null } - ?.let { - var addedWSS = - if (!it.startsWith("wss://") && !it.startsWith("ws://")) "wss://$it" else it - if (addedWSS.endsWith("/")) addedWSS = addedWSS.dropLast(1) - - addedWSS - } + ?.let { RelayUrlFormatter.normalize(it) } val privKeyHex = walletConnectSecret.text.ifBlank { null }?.let { decodePrivateKeyAsHexOrNull(it) } if (pubkeyHex != null) { - account?.changeZapPaymentRequest( + account.changeZapPaymentRequest( Nip47WalletConnect.Nip47URI( pubkeyHex, relayUrl, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d4959815f..921b29544 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -32,6 +32,7 @@ mockk = "1.13.11" navigationCompose = "2.7.7" okhttp = "5.0.0-alpha.14" runner = "1.5.2" +rfc3986 = "0.1.0" secp256k1KmpJniAndroid = "0.15.0" securityCryptoKtx = "1.1.0-alpha06" spotless = "6.25.0" @@ -100,6 +101,7 @@ markdown-ui = { group = "com.github.vitorpamplona.compose-richtext", name = "ric markdown-ui-material3 = { group = "com.github.vitorpamplona.compose-richtext", name = "richtext-ui-material3", version.ref = "markdown" } mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" } okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" } +rfc3986-normalizer = { group = "org.czeal", name = "rfc3986", version.ref = "rfc3986" } secp256k1-kmp-jni-android = { group = "fr.acinq.secp256k1", name = "secp256k1-kmp-jni-android", version.ref = "secp256k1KmpJniAndroid" } trbl-blurhash = { group = "io.trbl", name = "blurhash", version.ref = "blurhash" } unifiedpush = { group = "com.github.UnifiedPush", name = "android-connector", version.ref = "unifiedpush" } diff --git a/quartz/build.gradle b/quartz/build.gradle index bd34445b0..fb153c6f0 100644 --- a/quartz/build.gradle +++ b/quartz/build.gradle @@ -67,6 +67,9 @@ dependencies { // Parses URLs from Text: api libs.url.detector + // Parses URLs from Text: + api libs.rfc3986.normalizer + testImplementation libs.junit androidTestImplementation platform(libs.androidx.compose.bom) androidTestImplementation libs.androidx.junit diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/encoders/RelayUrlFormatter.kt b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/RelayUrlFormatter.kt index 7417f7209..eaa41f497 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/encoders/RelayUrlFormatter.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/RelayUrlFormatter.kt @@ -20,6 +20,8 @@ */ package com.vitorpamplona.quartz.encoders +import org.czeal.rfc3986.URIReference + class RelayUrlFormatter { companion object { fun displayUrl(url: String): String { @@ -27,20 +29,22 @@ class RelayUrlFormatter { } fun normalize(url: String): String { - var newUrl = + val newUrl = if (!url.startsWith("wss://") && !url.startsWith("ws://")) { if (url.endsWith(".onion") || url.endsWith(".onion/")) { - "ws://$url" + "ws://${url.trim()}" } else { - "wss://$url" + "wss://${url.trim()}" } } else { - url + url.trim() } - if (url.endsWith("/")) newUrl = newUrl.dropLast(1) - - return newUrl + return try { + URIReference.parse(newUrl).normalize().toString() + } catch (e: Exception) { + newUrl + } } fun getHttpsUrl(dirtyUrl: String): String {