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
This commit is contained in:
Claude
2026-04-03 12:01:34 +00:00
parent 7020eb003c
commit 0dc38de839
5 changed files with 30 additions and 0 deletions
@@ -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.
@@ -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<HexKey> = tags.mapNotNull(PTag::parseKey).toSet()
/** All group members: `p`-tagged pubkeys plus the event author. */
fun groupMembers(): Set<HexKey> = recipientPubKeys().plus(pubKey)
companion object {
const val KIND = 25051
const val ALT_DESCRIPTION = "WebRTC call answer"
@@ -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<HexKey> = tags.mapNotNull(PTag::parseKey).toSet()
/** All group members: `p`-tagged pubkeys plus the event author. */
fun groupMembers(): Set<HexKey> = recipientPubKeys().plus(pubKey)
companion object {
const val KIND = 25053
const val ALT_DESCRIPTION = "WebRTC call hangup"
@@ -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<HexKey> = tags.mapNotNull(PTag::parseKey).toSet()
/** All group members: `p`-tagged pubkeys plus the event author. */
fun groupMembers(): Set<HexKey> = recipientPubKeys().plus(pubKey)
companion object {
const val KIND = 25054
const val ALT_DESCRIPTION = "WebRTC call rejection"
@@ -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<HexKey> = tags.mapNotNull(PTag::parseKey).toSet()
/** All group members: `p`-tagged pubkeys plus the event author. */
fun groupMembers(): Set<HexKey> = recipientPubKeys().plus(pubKey)
companion object {
const val KIND = 25055
const val ALT_DESCRIPTION = "WebRTC call renegotiation"