From 0dc38de839bae8faeb035b723560740f7269840c Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 3 Apr 2026 12:01:34 +0000 Subject: [PATCH] feat: add recipientPubKeys()/groupMembers() helpers and multi-device docs - Add recipientPubKeys() and groupMembers() helper methods to CallAnswerEvent, CallHangupEvent, CallRejectEvent, and CallRenegotiateEvent (matching the existing pattern in CallOfferEvent) - Document in NIP-AC spec that group calls handle multi-device self-notification implicitly by including the sender's own pubkey in the gift-wrap recipient set https://claude.ai/code/session_013h2E7spwHDgSjunqumsYgp --- .../com/vitorpamplona/quartz/nipACWebRtcCalls/NIP-AC.md | 2 ++ .../quartz/nipACWebRtcCalls/events/CallAnswerEvent.kt | 7 +++++++ .../quartz/nipACWebRtcCalls/events/CallHangupEvent.kt | 7 +++++++ .../quartz/nipACWebRtcCalls/events/CallRejectEvent.kt | 7 +++++++ .../quartz/nipACWebRtcCalls/events/CallRenegotiateEvent.kt | 7 +++++++ 5 files changed, 30 insertions(+) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/NIP-AC.md b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/NIP-AC.md index f37ced8b8..58a30a365 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/NIP-AC.md +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/NIP-AC.md @@ -353,6 +353,8 @@ When a user is logged in on multiple devices, all devices will receive and ring These self-notification events use the same `call-id` as the original call and follow the same gift-wrapping rules. Clients receiving a self-addressed answer or reject MUST verify the `call-id` matches the currently ringing call before acting on it. +**Group calls**: In group calls, the sender's own pubkey SHOULD be included in the set of recipients when gift-wrapping answer and reject events. This means the self-notification is implicit — no separate self-addressed event is needed. The same signed inner event (with all group member `p` tags) is simply wrapped to the sender's own pubkey along with all other members. + ### Audio and Media - Clients SHOULD switch `AudioManager` to `MODE_IN_COMMUNICATION` when a call connects and restore to `MODE_NORMAL` when the call ends. diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/events/CallAnswerEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/events/CallAnswerEvent.kt index 22f74615e..3a5e892d0 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/events/CallAnswerEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/events/CallAnswerEvent.kt @@ -25,6 +25,7 @@ import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip01Core.tags.people.pTag import com.vitorpamplona.quartz.nip01Core.tags.people.pTagIds import com.vitorpamplona.quartz.nip31Alts.alt @@ -46,6 +47,12 @@ class CallAnswerEvent( fun sdpAnswer() = content + /** All pubkeys referenced by `p` tags in this event. */ + fun recipientPubKeys(): Set = tags.mapNotNull(PTag::parseKey).toSet() + + /** All group members: `p`-tagged pubkeys plus the event author. */ + fun groupMembers(): Set = recipientPubKeys().plus(pubKey) + companion object { const val KIND = 25051 const val ALT_DESCRIPTION = "WebRTC call answer" diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/events/CallHangupEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/events/CallHangupEvent.kt index e0a902301..cd953d18b 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/events/CallHangupEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/events/CallHangupEvent.kt @@ -25,6 +25,7 @@ import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip01Core.tags.people.pTag import com.vitorpamplona.quartz.nip01Core.tags.people.pTagIds import com.vitorpamplona.quartz.nip31Alts.alt @@ -46,6 +47,12 @@ class CallHangupEvent( fun reason() = content.ifEmpty { null } + /** All pubkeys referenced by `p` tags in this event. */ + fun recipientPubKeys(): Set = tags.mapNotNull(PTag::parseKey).toSet() + + /** All group members: `p`-tagged pubkeys plus the event author. */ + fun groupMembers(): Set = recipientPubKeys().plus(pubKey) + companion object { const val KIND = 25053 const val ALT_DESCRIPTION = "WebRTC call hangup" diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/events/CallRejectEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/events/CallRejectEvent.kt index a8ae74018..aad23a1ce 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/events/CallRejectEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/events/CallRejectEvent.kt @@ -25,6 +25,7 @@ import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip01Core.tags.people.pTag import com.vitorpamplona.quartz.nip01Core.tags.people.pTagIds import com.vitorpamplona.quartz.nip31Alts.alt @@ -46,6 +47,12 @@ class CallRejectEvent( fun reason() = content.ifEmpty { null } + /** All pubkeys referenced by `p` tags in this event. */ + fun recipientPubKeys(): Set = tags.mapNotNull(PTag::parseKey).toSet() + + /** All group members: `p`-tagged pubkeys plus the event author. */ + fun groupMembers(): Set = recipientPubKeys().plus(pubKey) + companion object { const val KIND = 25054 const val ALT_DESCRIPTION = "WebRTC call rejection" diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/events/CallRenegotiateEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/events/CallRenegotiateEvent.kt index e9a25024c..ba41f41be 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/events/CallRenegotiateEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/events/CallRenegotiateEvent.kt @@ -25,6 +25,7 @@ import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip01Core.tags.people.pTag import com.vitorpamplona.quartz.nip01Core.tags.people.pTagIds import com.vitorpamplona.quartz.nip31Alts.alt @@ -46,6 +47,12 @@ class CallRenegotiateEvent( fun sdpOffer() = content + /** All pubkeys referenced by `p` tags in this event. */ + fun recipientPubKeys(): Set = tags.mapNotNull(PTag::parseKey).toSet() + + /** All group members: `p`-tagged pubkeys plus the event author. */ + fun groupMembers(): Set = recipientPubKeys().plus(pubKey) + companion object { const val KIND = 25055 const val ALT_DESCRIPTION = "WebRTC call renegotiation"