From 473cfdd325423fff542b835e0406369ffd86bb0e Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Fri, 25 Sep 2026 07:40:50 -0300 Subject: [PATCH] Send feedback to the relays advertised on the Amber repo announcement The feedback issue was published to a hardcoded nos.lol with a synthetic repository-event hint. sendFeedBack now resolves the developer's NIP-65 relay list first, fetches the NIP-34 repository announcement (kind 30617, d=Amber) from it, and publishes the issue to the relays in its relays tag, using the real announcement as the issue's hint. When either fetch fails it falls back to the previous nos.lol behavior. On Tor both the fetch window (30s -> 60s) and the publish confirmation timeout (15s -> 60s) are widened, since Tor round trips routinely exceed the old limits. --- .../java/com/greenart7c3/nostrsigner/Amber.kt | 113 ++++++++++++++++-- 1 file changed, 101 insertions(+), 12 deletions(-) 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 } }