From ba06f9a92b06d8fb8abc46d35d2489688b72588a Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 15 Apr 2026 20:04:20 +0000 Subject: [PATCH] fix(marmot): deliver group welcome to the invitee's actual relays MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two related bugs in the Marmot "new group → add member" path were preventing the invited user from ever discovering the group, even though the commit applied cleanly on the creator's side. 1. fetchKeyPackageAndAddMember() searched for the invitee's KeyPackage on the *creator's* own outbox relays. KeyPackages are published by the invitee to their own outbox (see publishMarmotKeyPackage), so this only worked when the two happened to overlap. Union the invitee's NIP-65 outbox with ours before issuing fetchFirst(). 2. addMarmotGroupMember() routed the Welcome gift wrap through computeRelayListToBroadcast(). Its GiftWrap branch first tries the recipient's NIP-17 kind:10050 and falls back to computeRelayListForLinkedUser(), which reaches for inboxRelays()/relay hints and can legitimately return an empty set when the recipient has no cached NIP-17/NIP-65 data. client.publish then silently dropped the welcome, which is why the invitee saw nothing while the creator's local state looked healthy. Mirror sendNip04PrivateMessage's delivery strategy instead: publish to our own outbox (so we retain a copy and the invitee may find it via a shared relay) unioned with User.dmInboxRelays(), which already ladders NIP-17 kind:10050 → NIP-65 read relays — the exact set AccountGiftWrapsEoseManager listens on. A warning log covers the pathological case where both sides are empty. Note: spotlessApply/compile could not be run in the sandbox (Android Gradle plugin unreachable from this environment). Please run ./gradlew spotlessApply locally before merging. --- .../vitorpamplona/amethyst/model/Account.kt | 51 ++++++++++++++++--- 1 file changed, 45 insertions(+), 6 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 02d17cede..6aa8fddc7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -1757,10 +1757,23 @@ class Account( // Build filter for the member's KeyPackages val filter = manager.subscriptionManager.keyPackageFilter(memberPubKey) - val relays = outboxRelays.flow.value - // Query across outbox relays - val filterMap = relays.associateWith { listOf(filter) } + // KeyPackages are published by the invitee to *their own* outbox + // (see publishMarmotKeyPackage), so look there first. Union with + // our own outbox so we still find packages that ended up on a + // shared relay, and as a fallback when we don't know the + // invitee's outbox yet. + val myOutbox = outboxRelays.flow.value + val memberOutbox = + cache + .getOrCreateUser(memberPubKey) + .outboxRelays() + ?.toSet() + .orEmpty() + val fetchRelays = memberOutbox + myOutbox + + // Query across the combined relay set + val filterMap = fetchRelays.associateWith { listOf(filter) } val event = client.fetchFirst( @@ -1784,7 +1797,10 @@ class Account( kotlin.io.encoding.Base64 .decode(keyPackageBase64) val keyPackageEventId = event.id - val groupRelays = relays.toList() + // The relays embedded in the WelcomeEvent tell the new member + // where to subscribe for subsequent GroupEvents. Use our own + // outbox — that's where we will publish them. + val groupRelays = myOutbox.toList() addMarmotGroupMember( nostrGroupId = nostrGroupId, @@ -1823,9 +1839,32 @@ class Account( // Publish commit first (critical ordering) client.publish(commitEvent.signedEvent, groupRelays.toSet()) - // Then send Welcome gift wrap to the new member + // Then send the Welcome gift wrap to the new member. + // + // We deliberately do NOT route this through + // computeRelayListToBroadcast(): its GiftWrap branch returns an + // empty relay set when the recipient has no NIP-17 kind:10050 and + // no cached NIP-65 inbox relays, which silently drops the welcome + // and leaves the invitee with no way to discover the group. + // + // Instead, mirror sendNip04PrivateMessage's delivery strategy: + // publish to our own outbox (so we keep a copy and the invitee + // may find it via a shared relay) unioned with the recipient's + // DM inbox relays — User.dmInboxRelays() already falls back + // through NIP-17 kind:10050 → NIP-65 read relays, which is where + // the invitee's AccountGiftWrapsEoseManager actually listens. if (welcomeDelivery != null) { - val relayList = computeRelayListToBroadcast(welcomeDelivery.giftWrapEvent) + val recipientInbox = + cache + .getOrCreateUser(memberPubKey) + .dmInboxRelays() + .orEmpty() + val relayList = outboxRelays.flow.value + recipientInbox + if (relayList.isEmpty()) { + Log.w("Marmot") { + "addMarmotGroupMember: no relays to deliver welcome gift wrap to ${memberPubKey.take(8)}…" + } + } client.publish(welcomeDelivery.giftWrapEvent, relayList) } }