From 20504a5d5c71cf4bee62e12559a5c47ad6dd1b51 Mon Sep 17 00:00:00 2001 From: Barry Deen Date: Sun, 17 May 2026 10:09:52 -0400 Subject: [PATCH] fix(private-replies): fall back to recipient inbox relays, not our own MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the recipient hasn't published a kind 10050, fall back to their NIP-65 inbox (read) relays — fetching the kind 10002 list fresh from indexers if we haven't seen it yet — instead of silently dropping the wrap on our own write relays where the recipient never queries. Drop the prior write-relays / own-write-relays fallback chain entirely: neither is an inbox the recipient is listening on, so a wrap landing there was guaranteed lost. If we can't resolve any inbox we return sentCount=0 and surface the existing "no relays connected" error. PeerRelayListLookup is a small shared helper for the kind 10002 fetch. --- .../com/wisp/app/repo/PeerRelayListLookup.kt | 52 +++++++++++++++++++ .../wisp/app/repo/PrivateReplyPublisher.kt | 21 +++++--- 2 files changed, 65 insertions(+), 8 deletions(-) create mode 100644 app/src/main/kotlin/com/wisp/app/repo/PeerRelayListLookup.kt diff --git a/app/src/main/kotlin/com/wisp/app/repo/PeerRelayListLookup.kt b/app/src/main/kotlin/com/wisp/app/repo/PeerRelayListLookup.kt new file mode 100644 index 0000000..b6ca1e7 --- /dev/null +++ b/app/src/main/kotlin/com/wisp/app/repo/PeerRelayListLookup.kt @@ -0,0 +1,52 @@ +package com.wisp.app.repo + +import com.wisp.app.nostr.ClientMessage +import com.wisp.app.nostr.Filter +import com.wisp.app.relay.RelayConfig +import com.wisp.app.relay.RelayEvent +import com.wisp.app.relay.RelayPool +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.withTimeoutOrNull + +/** + * Fetch a peer's kind 10002 relay list (NIP-65) from indexer relays + the connected pool, + * then populate [RelayListRepository] with the freshest result. Caller can then ask the + * repository for the peer's read/inbox or write/outbox relays. + */ +object PeerRelayListLookup { + suspend fun fetch( + pubkey: String, + relayPool: RelayPool, + relayListRepo: RelayListRepository + ) { + val subId = "rl_${pubkey.take(8)}" + val filter = Filter( + kinds = listOf(10002), + authors = listOf(pubkey), + limit = 1 + ) + val reqMsg = ClientMessage.req(subId, filter) + for (url in RelayConfig.DEFAULT_INDEXER_RELAYS) { + relayPool.sendToRelayOrEphemeral(url, reqMsg, skipBadCheck = true) + } + relayPool.sendToAll(reqMsg) + + val results = mutableListOf() + withTimeoutOrNull(4000L) { + relayPool.relayEvents + .filter { it.subscriptionId == subId } + .collect { results.add(it) } + } + + val closeMsg = ClientMessage.close(subId) + for (url in RelayConfig.DEFAULT_INDEXER_RELAYS) { + relayPool.sendToRelay(url, closeMsg) + } + relayPool.sendToAll(closeMsg) + + val best = results.maxByOrNull { it.event.created_at } + if (best != null) { + relayListRepo.updateFromEvent(best.event) + } + } +} diff --git a/app/src/main/kotlin/com/wisp/app/repo/PrivateReplyPublisher.kt b/app/src/main/kotlin/com/wisp/app/repo/PrivateReplyPublisher.kt index f12a87d..be3b032 100644 --- a/app/src/main/kotlin/com/wisp/app/repo/PrivateReplyPublisher.kt +++ b/app/src/main/kotlin/com/wisp/app/repo/PrivateReplyPublisher.kt @@ -72,22 +72,27 @@ object PrivateReplyPublisher { createdAt = rumorCreatedAt ) + // Recipient resolution: their kind 10050 DM relays, or — if none — their NIP-65 + // inbox (read) relays. The recipient never queries our write relays, so we don't + // fall back to those: a wrap there would be silently lost. val recipientRelays: List = run { val dmRelays = DmRelayLookup.fetch(replyTo.pubkey, relayPool, dmRepo) if (dmRelays.isNotEmpty()) return@run dmRelays - relayListRepo?.getReadRelays(replyTo.pubkey)?.takeIf { it.isNotEmpty() }?.let { return@run it } - relayListRepo?.getWriteRelays(replyTo.pubkey)?.takeIf { it.isNotEmpty() }?.let { return@run it } + if (relayListRepo != null) { + relayListRepo.getReadRelays(replyTo.pubkey)?.takeIf { it.isNotEmpty() }?.let { return@run it } + // Cache miss — fetch kind 10002 fresh from indexers, then re-check. + PeerRelayListLookup.fetch(replyTo.pubkey, relayPool, relayListRepo) + relayListRepo.getReadRelays(replyTo.pubkey)?.takeIf { it.isNotEmpty() }?.let { return@run it } + } emptyList() } + if (recipientRelays.isEmpty()) return Result(0, null) + val recipientMsg = ClientMessage.event(recipientWrap) var sentCount = 0 - if (recipientRelays.isNotEmpty()) { - for (url in recipientRelays) { - if (relayPool.sendToRelayOrEphemeral(url, recipientMsg, skipBadCheck = true)) sentCount++ - } - } else { - sentCount += relayPool.sendToWriteRelays(recipientMsg) + for (url in recipientRelays) { + if (relayPool.sendToRelayOrEphemeral(url, recipientMsg, skipBadCheck = true)) sentCount++ } if (sentCount == 0) return Result(0, null)