diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/Amber.kt b/app/src/main/java/com/greenart7c3/nostrsigner/Amber.kt index 8a33ebba..1838e61e 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/Amber.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/Amber.kt @@ -60,16 +60,20 @@ import com.greenart7c3.nostrsigner.service.ZapstoreUpdater import com.greenart7c3.nostrsigner.service.crashreports.CrashReportCache import com.greenart7c3.nostrsigner.service.crashreports.UnexpectedCrashSaver import com.greenart7c3.nostrsigner.ui.ToastManager +import com.vitorpamplona.quartz.nip01Core.crypto.verify import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.fetchFirst import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.publishAndConfirm import com.vitorpamplona.quartz.nip01Core.relay.client.auth.RelayAuthenticator import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayStats +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip34Git.issue.GitIssueEvent import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent +import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.utils.TimeUtils import java.lang.ref.WeakReference import java.net.InetSocketAddress @@ -782,18 +786,18 @@ class Amber : type: FeedbackType, account: Account, ): Boolean { - val relays = setOfNotNull( - RelayUrlNormalizer.normalizeOrNull("wss://nos.lol/"), - ) + val usingTor = settings.torMode != TorMode.DISABLED + // Tor round trips are slow; give the relays time to answer OK before + // declaring the publish failed. + val publishTimeoutSeconds = + if (usingTor) FEEDBACK_TOR_PUBLISH_TIMEOUT_SECONDS else FEEDBACK_PUBLISH_TIMEOUT_SECONDS - val repositoryEvent = GitRepositoryEvent( - "", - DEVELOPER_HEX_KEY, - TimeUtils.now(), - tags = arrayOf(arrayOf("d", "Amber")), - "", - "", - ) + val repositoryEvent = fetchAmberRepositoryEvent(usingTor) ?: fallbackRepositoryEvent() + val relays = repositoryEvent.relays() + .mapNotNull { RelayUrlNormalizer.normalizeOrNull(it) } + .toSet() + .takeIf { it.isNotEmpty() } + ?: setOfNotNull(RelayUrlNormalizer.normalizeOrNull(FEEDBACK_FALLBACK_RELAY)) val template = GitIssueEvent.build( subject, @@ -810,9 +814,88 @@ class Amber : // This will automatically connect to these relays even if they are not in the list // once the events have been saved, it will disconnect from them unless there are other // filters with them - return client.publishAndConfirm(event, relays) + return client.publishAndConfirm(event, relays, publishTimeoutSeconds) } + /** + * NIP-34: a repository announcement's `relays` tag lists the relays its + * issues live on. The announcement itself is published on the author's own + * relays, so resolve those from their NIP-65 relay list first, then ask + * for the announcement. Returns null when either step finds nothing. + */ + private suspend fun fetchAmberRepositoryEvent(usingTor: Boolean): GitRepositoryEvent? { + val fetchTimeoutMs = if (usingTor) FEEDBACK_TOR_FETCH_TIMEOUT_MS else FEEDBACK_FETCH_TIMEOUT_MS + + val authorRelays = fetchAuthorRelays(fetchTimeoutMs) + val discoveryRelays = (authorRelays + settings.defaultRelays) + .takeIf { it.isNotEmpty() } + ?: setOfNotNull(RelayUrlNormalizer.normalizeOrNull(FEEDBACK_FALLBACK_RELAY)) + + val event = client.fetchFirst( + filters = discoveryRelays.associateWith { + listOf( + Filter( + kinds = listOf(GitRepositoryEvent.KIND), + authors = listOf(DEVELOPER_HEX_KEY), + tags = mapOf("d" to listOf(AMBER_REPO_IDENTIFIER)), + limit = 1, + ), + ) + }, + idleTimeoutMs = fetchTimeoutMs, + ) ?: return null + if (!event.verify()) return null + + return GitRepositoryEvent( + event.id, + event.pubKey, + event.createdAt, + event.tags, + event.content, + event.sig, + ) + } + + private suspend fun fetchAuthorRelays(fetchTimeoutMs: Long): Set { + val discoveryRelays = (settings.defaultProfileRelays + settings.defaultRelays) + .toSet() + .takeIf { it.isNotEmpty() } + ?: setOfNotNull(RelayUrlNormalizer.normalizeOrNull(FEEDBACK_FALLBACK_RELAY)) + + val event = client.fetchFirst( + filters = discoveryRelays.associateWith { + listOf( + Filter( + kinds = listOf(AdvertisedRelayListEvent.KIND), + authors = listOf(DEVELOPER_HEX_KEY), + limit = 1, + ), + ) + }, + idleTimeoutMs = fetchTimeoutMs, + ) ?: return emptySet() + if (!event.verify()) return emptySet() + + return AdvertisedRelayListEvent( + event.id, + event.pubKey, + event.createdAt, + event.tags, + event.content, + event.sig, + ).relaysNorm().toSet() + } + + /** Stand-in used for the issue's `a` tag hint when no announcement was found. */ + private fun fallbackRepositoryEvent(): GitRepositoryEvent = GitRepositoryEvent( + "", + DEVELOPER_HEX_KEY, + TimeUtils.now(), + tags = arrayOf(arrayOf("d", AMBER_REPO_IDENTIFIER)), + "", + "", + ) + companion object { var isAppInForeground = false const val TAG = "Amber" @@ -821,6 +904,12 @@ class Amber : private set const val DEVELOPER_HEX_KEY = "7579076d9aff0a4cfdefa7e2045f2486c7e5d8bc63bfc6b45397233e1bbfcb19" + private const val AMBER_REPO_IDENTIFIER = "Amber" + private const val FEEDBACK_FALLBACK_RELAY = "wss://nos.lol/" + private const val FEEDBACK_PUBLISH_TIMEOUT_SECONDS = 15L + private const val FEEDBACK_TOR_PUBLISH_TIMEOUT_SECONDS = 60L + private const val FEEDBACK_FETCH_TIMEOUT_MS = 30_000L + private const val FEEDBACK_TOR_FETCH_TIMEOUT_MS = 60_000L } }