fix(private-replies): fall back to recipient inbox relays, not our own

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.
This commit is contained in:
Barry Deen
2026-05-17 10:09:52 -04:00
parent 4e98611712
commit 20504a5d5c
2 changed files with 65 additions and 8 deletions
@@ -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<RelayEvent>()
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)
}
}
}
@@ -72,23 +72,28 @@ 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<String> = 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)
}
if (sentCount == 0) return Result(0, null)